diff options
author | Luke "Jared" Bennett <lbennett@gitlab.com> | 2017-08-18 21:14:26 +0300 |
---|---|---|
committer | Luke "Jared" Bennett <lbennett@gitlab.com> | 2017-08-18 21:14:26 +0300 |
commit | 2998ef8746fa0754cc6d9c315a114f8e48bc29ed (patch) | |
tree | a57a88f4bd79e4aedec754362a898e1f839196ef | |
parent | 6b023c1e0be1b72c3d3a54712dc9ff77a920028d (diff) | |
parent | 338ab50548750a6fadca891600826c5aaf76dbff (diff) |
Merge branch 'link-to-line' of gitlab.com:gitlab-org/gitlab-ce into link-to-line
851 files changed, 11586 insertions, 5111 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f55bdc19eec..df7244d5a2e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-7.1-postgresql-9.6" .default-cache: &default-cache key: "ruby-233-with-yarn" @@ -27,6 +27,7 @@ variables: GET_SOURCES_ATTEMPTS: "3" KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json + FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json before_script: - bundle --version @@ -45,16 +46,17 @@ stages: tags: - gitlab-org -.knapsack-state: &knapsack-state +.tests-metadata-state: &tests-metadata-state services: [] variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" - KNAPSACK_S3_BUCKET: "gitlab-ce-cache" + TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache" artifacts: expire_in: 31d paths: - knapsack/ + - rspec_flaky/ .use-pg: &use-pg services: @@ -86,7 +88,7 @@ stages: except: - /(^docs[\/-].*|.*-docs$)/ -.rspec-knapsack: &rspec-knapsack +.rspec-metadata: &rspec-metadata <<: *dedicated-runner <<: *pull-cache stage: test @@ -96,8 +98,13 @@ stages: - export CI_NODE_TOTAL=${JOB_NAME[-1]} - 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 ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export FLAKY_RSPEC_GENERATE_REPORT=true - export CACHE_CLASSES=true - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} + - cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH} + - '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}' - scripts/gitaly-test-spawn - knapsack rspec "--color --format documentation" artifacts: @@ -106,20 +113,21 @@ stages: paths: - coverage/ - knapsack/ + - rspec_flaky/ - tmp/capybara/ -.rspec-knapsack-pg: &rspec-knapsack-pg - <<: *rspec-knapsack +.rspec-metadata-pg: &rspec-metadata-pg + <<: *rspec-metadata <<: *use-pg <<: *except-docs -.rspec-knapsack-mysql: &rspec-knapsack-mysql - <<: *rspec-knapsack +.rspec-metadata-mysql: &rspec-metadata-mysql + <<: *rspec-metadata <<: *use-mysql <<: *only-if-want-mysql <<: *except-docs -.spinach-knapsack: &spinach-knapsack +.spinach-metadata: &spinach-metadata <<: *dedicated-runner <<: *pull-cache stage: test @@ -140,13 +148,13 @@ stages: - knapsack/ - tmp/capybara/ -.spinach-knapsack-pg: &spinach-knapsack-pg - <<: *spinach-knapsack +.spinach-metadata-pg: &spinach-metadata-pg + <<: *spinach-metadata <<: *use-pg <<: *except-docs -.spinach-knapsack-mysql: &spinach-knapsack-mysql - <<: *spinach-knapsack +.spinach-metadata-mysql: &spinach-metadata-mysql + <<: *spinach-metadata <<: *use-mysql <<: *only-if-want-mysql <<: *except-docs @@ -176,40 +184,71 @@ build-package: - //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ee -# Prepare and merge knapsack tests -knapsack: - <<: *knapsack-state +# Retrieve knapsack and rspec_flaky reports +retrieve-tests-metadata: + <<: *tests-metadata-state <<: *dedicated-runner <<: *except-docs stage: prepare cache: - key: knapsack - paths: - - knapsack/ + key: tests_metadata policy: pull script: - mkdir -p knapsack/${CI_PROJECT_NAME}/ - - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH - - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH + - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH + - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' + - mkdir -p rspec_flaky/${CI_PROJECT_NAME}/ + - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH + - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' -update-knapsack: - <<: *knapsack-state +update-tests-metadata: + <<: *tests-metadata-state <<: *dedicated-runner <<: *only-canonical-masters stage: post-test cache: - key: knapsack + key: tests_metadata paths: - knapsack/ + - rspec_flaky/ policy: push script: - retry gem install fog-aws mime-types - 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' + - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json + - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' + - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json + - rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json + +flaky-examples-check: + <<: *dedicated-runner + <<: *except-docs + image: ruby:2.3-alpine + services: [] + before_script: [] + cache: {} + variables: + SETUP_DB: "false" + USE_BUNDLE_INSTALL: "false" + NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json + stage: post-test + allow_failure: yes + only: + - branches + except: + - master + artifacts: + expire_in: 30d + paths: + - rspec_flaky/ + script: + - '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}' + - scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json + - scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT setup-test-env: <<: *use-pg @@ -232,69 +271,69 @@ setup-test-env: - public/assets - tmp/tests -rspec-pg 0 25: *rspec-knapsack-pg -rspec-pg 1 25: *rspec-knapsack-pg -rspec-pg 2 25: *rspec-knapsack-pg -rspec-pg 3 25: *rspec-knapsack-pg -rspec-pg 4 25: *rspec-knapsack-pg -rspec-pg 5 25: *rspec-knapsack-pg -rspec-pg 6 25: *rspec-knapsack-pg -rspec-pg 7 25: *rspec-knapsack-pg -rspec-pg 8 25: *rspec-knapsack-pg -rspec-pg 9 25: *rspec-knapsack-pg -rspec-pg 10 25: *rspec-knapsack-pg -rspec-pg 11 25: *rspec-knapsack-pg -rspec-pg 12 25: *rspec-knapsack-pg -rspec-pg 13 25: *rspec-knapsack-pg -rspec-pg 14 25: *rspec-knapsack-pg -rspec-pg 15 25: *rspec-knapsack-pg -rspec-pg 16 25: *rspec-knapsack-pg -rspec-pg 17 25: *rspec-knapsack-pg -rspec-pg 18 25: *rspec-knapsack-pg -rspec-pg 19 25: *rspec-knapsack-pg -rspec-pg 20 25: *rspec-knapsack-pg -rspec-pg 21 25: *rspec-knapsack-pg -rspec-pg 22 25: *rspec-knapsack-pg -rspec-pg 23 25: *rspec-knapsack-pg -rspec-pg 24 25: *rspec-knapsack-pg - -rspec-mysql 0 25: *rspec-knapsack-mysql -rspec-mysql 1 25: *rspec-knapsack-mysql -rspec-mysql 2 25: *rspec-knapsack-mysql -rspec-mysql 3 25: *rspec-knapsack-mysql -rspec-mysql 4 25: *rspec-knapsack-mysql -rspec-mysql 5 25: *rspec-knapsack-mysql -rspec-mysql 6 25: *rspec-knapsack-mysql -rspec-mysql 7 25: *rspec-knapsack-mysql -rspec-mysql 8 25: *rspec-knapsack-mysql -rspec-mysql 9 25: *rspec-knapsack-mysql -rspec-mysql 10 25: *rspec-knapsack-mysql -rspec-mysql 11 25: *rspec-knapsack-mysql -rspec-mysql 12 25: *rspec-knapsack-mysql -rspec-mysql 13 25: *rspec-knapsack-mysql -rspec-mysql 14 25: *rspec-knapsack-mysql -rspec-mysql 15 25: *rspec-knapsack-mysql -rspec-mysql 16 25: *rspec-knapsack-mysql -rspec-mysql 17 25: *rspec-knapsack-mysql -rspec-mysql 18 25: *rspec-knapsack-mysql -rspec-mysql 19 25: *rspec-knapsack-mysql -rspec-mysql 20 25: *rspec-knapsack-mysql -rspec-mysql 21 25: *rspec-knapsack-mysql -rspec-mysql 22 25: *rspec-knapsack-mysql -rspec-mysql 23 25: *rspec-knapsack-mysql -rspec-mysql 24 25: *rspec-knapsack-mysql - -spinach-pg 0 5: *spinach-knapsack-pg -spinach-pg 1 5: *spinach-knapsack-pg -spinach-pg 2 5: *spinach-knapsack-pg -spinach-pg 3 5: *spinach-knapsack-pg -spinach-pg 4 5: *spinach-knapsack-pg - -spinach-mysql 0 5: *spinach-knapsack-mysql -spinach-mysql 1 5: *spinach-knapsack-mysql -spinach-mysql 2 5: *spinach-knapsack-mysql -spinach-mysql 3 5: *spinach-knapsack-mysql -spinach-mysql 4 5: *spinach-knapsack-mysql +rspec-pg 0 25: *rspec-metadata-pg +rspec-pg 1 25: *rspec-metadata-pg +rspec-pg 2 25: *rspec-metadata-pg +rspec-pg 3 25: *rspec-metadata-pg +rspec-pg 4 25: *rspec-metadata-pg +rspec-pg 5 25: *rspec-metadata-pg +rspec-pg 6 25: *rspec-metadata-pg +rspec-pg 7 25: *rspec-metadata-pg +rspec-pg 8 25: *rspec-metadata-pg +rspec-pg 9 25: *rspec-metadata-pg +rspec-pg 10 25: *rspec-metadata-pg +rspec-pg 11 25: *rspec-metadata-pg +rspec-pg 12 25: *rspec-metadata-pg +rspec-pg 13 25: *rspec-metadata-pg +rspec-pg 14 25: *rspec-metadata-pg +rspec-pg 15 25: *rspec-metadata-pg +rspec-pg 16 25: *rspec-metadata-pg +rspec-pg 17 25: *rspec-metadata-pg +rspec-pg 18 25: *rspec-metadata-pg +rspec-pg 19 25: *rspec-metadata-pg +rspec-pg 20 25: *rspec-metadata-pg +rspec-pg 21 25: *rspec-metadata-pg +rspec-pg 22 25: *rspec-metadata-pg +rspec-pg 23 25: *rspec-metadata-pg +rspec-pg 24 25: *rspec-metadata-pg + +rspec-mysql 0 25: *rspec-metadata-mysql +rspec-mysql 1 25: *rspec-metadata-mysql +rspec-mysql 2 25: *rspec-metadata-mysql +rspec-mysql 3 25: *rspec-metadata-mysql +rspec-mysql 4 25: *rspec-metadata-mysql +rspec-mysql 5 25: *rspec-metadata-mysql +rspec-mysql 6 25: *rspec-metadata-mysql +rspec-mysql 7 25: *rspec-metadata-mysql +rspec-mysql 8 25: *rspec-metadata-mysql +rspec-mysql 9 25: *rspec-metadata-mysql +rspec-mysql 10 25: *rspec-metadata-mysql +rspec-mysql 11 25: *rspec-metadata-mysql +rspec-mysql 12 25: *rspec-metadata-mysql +rspec-mysql 13 25: *rspec-metadata-mysql +rspec-mysql 14 25: *rspec-metadata-mysql +rspec-mysql 15 25: *rspec-metadata-mysql +rspec-mysql 16 25: *rspec-metadata-mysql +rspec-mysql 17 25: *rspec-metadata-mysql +rspec-mysql 18 25: *rspec-metadata-mysql +rspec-mysql 19 25: *rspec-metadata-mysql +rspec-mysql 20 25: *rspec-metadata-mysql +rspec-mysql 21 25: *rspec-metadata-mysql +rspec-mysql 22 25: *rspec-metadata-mysql +rspec-mysql 23 25: *rspec-metadata-mysql +rspec-mysql 24 25: *rspec-metadata-mysql + +spinach-pg 0 5: *spinach-metadata-pg +spinach-pg 1 5: *spinach-metadata-pg +spinach-pg 2 5: *spinach-metadata-pg +spinach-pg 3 5: *spinach-metadata-pg +spinach-pg 4 5: *spinach-metadata-pg + +spinach-mysql 0 5: *spinach-metadata-mysql +spinach-mysql 1 5: *spinach-metadata-mysql +spinach-mysql 2 5: *spinach-metadata-mysql +spinach-mysql 3 5: *spinach-metadata-mysql +spinach-mysql 4 5: *spinach-metadata-mysql # Static analysis jobs .ruby-static-analysis: &ruby-static-analysis @@ -354,7 +393,7 @@ ee_compat_check: except: - master - tags - - /^[\d-]+-stable(-ee)?$/ + - /^[\d-]+-stable(-ee)?/ allow_failure: yes cache: key: "ee_compat_check_repo" @@ -435,7 +474,6 @@ db:rollback-mysql: variables: SIZE: "1" SETUP_DB: "false" - RAILS_ENV: "development" script: - git clone https://gitlab.com/gitlab-org/gitlab-test.git /home/git/repositories/gitlab-org/gitlab-test.git @@ -484,7 +522,7 @@ karma: <<: *dedicated-runner <<: *except-docs <<: *pull-cache - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6" + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-59.0-node-7.1-postgresql-9.6" stage: test variables: BABEL_ENV: "coverage" @@ -514,8 +552,11 @@ codeclimate: services: - docker:dind script: + - cp .rubocop.yml .rubocop.yml.bak + - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml - 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 > raw_codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json + - mv .rubocop.yml.bak .rubocop.yml artifacts: paths: [codeclimate.json] diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md new file mode 100644 index 00000000000..2a5c8267872 --- /dev/null +++ b/.gitlab/merge_request_templates/Database Changes.md @@ -0,0 +1,73 @@ +Remove this section and replace it with a description of your MR. Also follow the +checklist below and check off any tasks that are done. If a certain task can not +be done you should explain so in the MR body. You are free to remove any +sections that do not apply to your MR. + +When gathering statistics (e.g. the output of `EXPLAIN ANALYZE`) you should make +sure your database has enough data. Having around 10 000 rows in the tables +being queries should provide a reasonable estimate of how a query will behave. +Also make sure that PostgreSQL uses the following settings: + +* `random_page_cost`: `1` +* `work_mem`: `16MB` +* `maintenance_work_mem`: at least `64MB` +* `shared_buffers`: at least `256MB` + +If you have access to GitLab.com's staging environment you should also run your +measurements there, and include the results in this MR. + +## Database Checklist + +When adding migrations: + +- [ ] Updated `db/schema.rb` +- [ ] Added a `down` method so the migration can be reverted +- [ ] Added the output of the migration(s) to the MR body +- [ ] Added the execution time of the migration(s) to the MR body +- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when + migrating data) +- [ ] Made sure the migration won't interfere with a running GitLab cluster, + for example by disabling transactions for long running migrations + +When adding or modifying queries to improve performance: + +- [ ] Included the raw SQL queries of the relevant queries +- [ ] Included the output of `EXPLAIN ANALYZE` and execution timings of the + relevant queries +- [ ] Added tests for the relevant changes + +When adding indexes: + +- [ ] Described the need for these indexes in the MR body +- [ ] Made sure existing indexes can not be reused instead + +When adding foreign keys to existing tables: + +- [ ] Included a migration to remove orphaned rows in the source table +- [ ] Removed any instances of `dependent: ...` that may no longer be necessary + +When adding tables: + +- [ ] Ordered columns based on their type sizes in descending order +- [ ] Added foreign keys if necessary +- [ ] Added indexes if necessary + +When removing columns, tables, indexes or other structures: + +- [ ] Removed these in a post-deployment migration +- [ ] Made sure the application no longer uses (or ignores) these structures + +## General Checklist + +- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary +- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) +- [ ] API support added +- [ ] Tests added for this feature/bug +- Review + - [ ] Has been reviewed by UX + - [ ] Has been reviewed by Frontend + - [ ] Has been reviewed by Backend + - [ ] Has been reviewed by Database +- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) +- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) +- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) diff --git a/.rubocop.yml b/.rubocop.yml index a5ccec0437b..23bb0fa8be8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,6 @@ require: - rubocop-rspec + - rubocop-gitlab-security - ./rubocop/rubocop inherit_from: .rubocop_todo.yml @@ -206,6 +207,13 @@ Layout/SpaceAroundKeyword: Layout/SpaceAroundOperators: Enabled: true +# Checks that block braces have or don't have a space before the opening +# brace depending on configuration. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: space, no_space +Layout/SpaceBeforeBlockBraces: + Enabled: true + # No spaces before commas. Layout/SpaceBeforeComma: Enabled: true @@ -243,6 +251,10 @@ Layout/Tab: Layout/TrailingBlankLines: Enabled: true +# Avoid trailing whitespace. +Layout/TrailingWhitespace: + Enabled: true + # Style ####################################################################### # Check the naming of accessor methods for get_/set_. @@ -1037,7 +1049,7 @@ RSpec/BeforeAfterAll: RSpec/DescribeClass: Enabled: false -# Use `described_class` for tested class / module. +# Checks that the second argument to `describe` specifies a method. RSpec/DescribeMethod: Enabled: false @@ -1045,8 +1057,7 @@ RSpec/DescribeMethod: RSpec/DescribeSymbol: Enabled: true -# Checks that the second argument to top level describe is the tested method -# name. +# Checks that tests use `described_class`. RSpec/DescribedClass: Enabled: true @@ -1091,6 +1102,11 @@ RSpec/FilePath: RSpec/Focus: Enabled: true +# Checks the arguments passed to `before`, `around`, and `after`. +RSpec/HookArgument: + Enabled: true + EnforcedStyle: implicit + # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: is_expected, should RSpec/ImplicitExpect: @@ -1156,3 +1172,39 @@ RSpec/SubjectStub: # Prefer using verifying doubles over normal doubles. RSpec/VerifiedDoubles: Enabled: false + +# GitlabSecurity ############################################################## + +GitlabSecurity/DeepMunge: + Enabled: true + Exclude: + - 'lib/**/*.rake' + - 'spec/**/*' + +GitlabSecurity/PublicSend: + Enabled: true + Exclude: + - 'config/**/*' + - 'db/**/*' + - 'features/**/*' + - 'lib/**/*.rake' + - 'qa/**/*' + - 'spec/**/*' + +GitlabSecurity/RedirectToParamsUpdate: + Enabled: true + Exclude: + - 'lib/**/*.rake' + - 'spec/**/*' + +GitlabSecurity/SqlInjection: + Enabled: true + Exclude: + - 'lib/**/*.rake' + - 'spec/**/*' + +GitlabSecurity/SystemCommandInjection: + Enabled: true + Exclude: + - 'lib/**/*.rake' + - 'spec/**/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9caef3bde08..4b4f14efea4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -26,13 +26,6 @@ Layout/IndentArray: Layout/IndentHash: Enabled: false -# Offense count: 174 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: space, no_space -Layout/SpaceBeforeBlockBraces: - Enabled: false - # Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. @@ -64,11 +57,6 @@ Layout/SpaceInsideParens: Layout/SpaceInsidePercentLiteralDelimiters: Enabled: false -# Offense count: 89 -# Cop supports --auto-correct. -Layout/TrailingWhitespace: - Enabled: false - # Offense count: 272 RSpec/EmptyLineAfterFinalLet: Enabled: false @@ -77,12 +65,6 @@ RSpec/EmptyLineAfterFinalLet: RSpec/EmptyLineAfterSubject: Enabled: false -# Offense count: 78 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: implicit, each, example -RSpec/HookArgument: - Enabled: false - # Offense count: 9 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: it_behaves_like, it_should_behave_like diff --git a/CHANGELOG.md b/CHANGELOG.md index 7493f2562e8..3ecedd44c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.4.5 (2017-08-14) + +- Fix deletion of deploy keys linked to other projects. !13162 +- Allow any logged in users to read_users_list even if it's restricted. !13201 +- Make Delete Merged Branches handle wildcard protected branches correctly. !13251 +- Fix an order of operations for CI connection error message in merge request widget. !13252 +- Fix pipeline_schedules pages when active schedule has an abnormal state. !13286 +- Add missing validation error for username change with container registry tags. !13356 +- Fix destroy of case-insensitive conflicting redirects. !13357 +- Project pending delete no longer return 500 error in admins projects view. !13389 +- Fix search box losing focus when typing. +- Use jQuery to control scroll behavior in job log for cross browser consistency. +- Use project_ref_path to create the link to a branch to fix links that 404. +- improve file upload/replace experience. +- fix jump to next discussion button. +- Fixes new issue button for failed job returning 404. +- Fix links to group milestones from issue and merge request sidebar. +- Fixed sign-in restrictions buttons not toggling active state. +- Fix Mattermost integration. +- Change project FK migration to skip existing FKs. + +## 9.4.4 (2017-08-09) + +- Remove hidden symlinks from project import files. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. + ## 9.4.3 (2017-07-31) - Fix Prometheus client PID reuse bug. !13130 @@ -226,6 +252,11 @@ entry. - Log rescued exceptions to Sentry. - Remove remaining N+1 queries in merge requests API with emojis and labels. +## 9.3.10 (2017-08-09) + +- Remove hidden symlinks from project import files. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. + ## 9.3.9 (2017-07-20) - Fix an infinite loop when handling user-supplied regular expressions. @@ -498,6 +529,11 @@ entry. - Remove foreigh key on ci_trigger_schedules only if it exists. - Allow translation of Pipeline Schedules. +## 9.2.10 (2017-08-09) + +- Remove hidden symlinks from project import files. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. + ## 9.2.9 (2017-07-20) - Fix an infinite loop when handling user-supplied regular expressions. @@ -753,6 +789,11 @@ entry. - Fix preemptive scroll bar on user activity calendar. - Pipeline chat notifications convert seconds to minutes and hours. +## 9.1.10 (2017-08-09) + +- Remove hidden symlinks from project import files. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. + ## 9.1.9 (2017-07-20) - Fix an infinite loop when handling user-supplied regular expressions. @@ -1076,6 +1117,11 @@ entry. - Only send chat notifications for the default branch. - Don't fill in the default kubernetes namespace. +## 9.0.13 (2017-08-09) + +- Remove hidden symlinks from project import files. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. + ## 9.0.12 (2017-07-20) - Fix an infinite loop when handling user-supplied regular expressions. @@ -1456,6 +1502,11 @@ entry. - Change development tanuki favicon colors to match logo color order. - API issues - support filtering by iids. +## 8.17.8 (2017-08-09) + +- Remove hidden symlinks from project import files. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. + ## 8.17.7 (2017-07-19) - Renders 404 if given project is not readable by the user on Todos dashboard. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ae6dd4e2032..c25c8e5b741 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.29.0 +0.30.0 @@ -16,7 +16,7 @@ gem 'mysql2', '~> 0.4.5', group: :mysql gem 'pg', '~> 0.18.2', group: :postgres gem 'rugged', '~> 0.26.0' -gem 'grape-route-helpers', '~> 2.0.0' +gem 'grape-route-helpers', '~> 2.1.0' gem 'faraday', '~> 0.12' @@ -64,7 +64,7 @@ gem 'gpgme' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '~> 2.0.3', require: 'omniauth-ldap' +gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap' gem 'net-ldap' # Git Wiki @@ -76,7 +76,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.4', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.19.2' +gem 'grape', '~> 1.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' @@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'hashie-forbidden_attributes' # Pagination -gem 'kaminari', '~> 0.17.0' +gem 'kaminari', '~> 1.0' # HAML gem 'hamlit', '~> 2.6.1' @@ -144,8 +144,6 @@ end # State machine gem 'state_machines-activerecord', '~> 0.4.0' -# Run events after state machine commits -gem 'after_commit_queue', '~> 1.3.0' # Issue tags gem 'acts-as-taggable-on', '~> 4.0' @@ -232,7 +230,7 @@ gem 'ace-rails-ap', '~> 4.1.0' gem 'mousetrap-rails', '~> 1.4.6' # Detect and convert string character encoding -gem 'charlock_holmes', '~> 0.7.3' +gem 'charlock_holmes', '~> 0.7.5' # Faster JSON gem 'oj', '~> 2.17.4' @@ -324,6 +322,7 @@ group :development, :test do gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'rspec_profiling', '~> 0.0.5' gem 'rspec-set', '~> 0.1.3' + gem 'rspec-parameterized' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.7.0' @@ -341,6 +340,7 @@ group :development, :test do gem 'rubocop', '~> 0.49.1', require: false gem 'rubocop-rspec', '~> 1.15.1', require: false + gem 'rubocop-gitlab-security', '~> 0.0.6', require: false gem 'scss_lint', '~> 0.54.0', require: false gem 'haml_lint', '~> 0.26.0', require: false gem 'simplecov', '~> 0.14.0', require: false @@ -354,7 +354,7 @@ group :development, :test do gem 'activerecord_sane_schema_dumper', '0.2' - gem 'stackprof', '~> 0.2.10' + gem 'stackprof', '~> 0.2.10', require: false end group :test do @@ -401,7 +401,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly', '~> 0.26.0' +gem 'gitaly', '~> 0.27.0' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 948ba02a72c..ae1df783a80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ GEM remote: https://rubygems.org/ specs: RedCloth (4.3.2) + abstract_type (0.0.7) ace-rails-ap (4.1.2) actionmailer (4.2.8) actionpack (= 4.2.8) @@ -41,9 +42,10 @@ GEM tzinfo (~> 1.1) acts-as-taggable-on (4.0.0) activerecord (>= 4.0) + adamantium (0.2.0) + ice_nine (~> 0.11.0) + memoizable (~> 0.4.0) addressable (2.3.8) - after_commit_queue (1.3.0) - activerecord (>= 3.0) akismet (2.0.0) allocations (1.0.5) arel (6.0.4) @@ -113,7 +115,7 @@ GEM activesupport (>= 4.0.0) mime-types (>= 1.16) cause (0.1) - charlock_holmes (0.7.3) + charlock_holmes (0.7.5) chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) @@ -124,6 +126,9 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.7.7) + concord (0.1.5) + adamantium (~> 0.2.0) + equalizer (~> 0.0.9) concurrent-ruby (1.0.5) concurrent-ruby-ext (1.0.5) concurrent-ruby (= 1.0.5) @@ -270,7 +275,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly (0.26.0) + gitaly (0.27.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -289,7 +294,7 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) - gitlab_omniauth-ldap (2.0.3) + gitlab_omniauth-ldap (2.0.4) net-ldap (~> 0.16) omniauth (~> 1.3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) @@ -335,12 +340,9 @@ GEM signet (~> 0.7) gpgme (2.0.13) mini_portile2 (~> 2.1) - grape (0.19.2) + grape (1.0.0) activesupport builder - hashie (>= 2.1.0) - multi_json (>= 1.3.2) - multi_xml (>= 0.5.2) mustermann-grape (~> 1.0.0) rack (>= 1.3.0) rack-accept @@ -348,11 +350,11 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) - grape-route-helpers (2.0.0) + grape-route-helpers (2.1.0) activesupport - grape (~> 0.16, >= 0.16.0) + grape (>= 0.16.0) rake - grpc (1.4.0) + grpc (1.4.5) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) @@ -368,7 +370,7 @@ GEM thor tilt hashdiff (0.3.4) - hashie (3.5.5) + hashie (3.5.6) hashie-forbidden_attributes (0.1.1) hashie (>= 3.0) health_check (2.6.0) @@ -419,9 +421,18 @@ GEM json-schema (2.6.2) addressable (~> 2.3.8) jwt (1.5.6) - kaminari (0.17.0) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) kgio (2.10.0) knapsack (1.11.0) rake @@ -461,6 +472,8 @@ GEM mime-types (>= 1.16, < 4) mail_room (0.9.1) memoist (0.15.0) + memoizable (0.4.2) + thread_safe (~> 0.3, >= 0.3.1) method_source (0.8.2) mime-types (2.99.3) mimemagic (0.3.0) @@ -548,7 +561,7 @@ GEM rubypants (~> 0.2) orm_adapter (0.5.0) os (0.9.6) - parallel (1.11.2) + parallel (1.12.0) paranoia (2.3.1) activerecord (>= 4.0, < 5.2) parser (2.4.0.0) @@ -601,6 +614,11 @@ GEM premailer-rails (1.9.7) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) + proc_to_ast (0.1.0) + coderay + parser + unparser + procto (0.0.3) prometheus-client-mmap (0.7.0.beta11) mmap2 (~> 2.2, >= 2.2.7) pry (0.10.4) @@ -709,6 +727,10 @@ GEM chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) + rspec (3.6.0) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) rspec-core (3.6.0) rspec-support (~> 3.6.0) rspec-expectations (3.6.0) @@ -717,6 +739,12 @@ GEM rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) + rspec-parameterized (0.4.0) + binding_of_caller + parser + proc_to_ast + rspec (>= 2.13, < 4) + unparser rspec-rails (3.6.0) actionpack (>= 3.0) activesupport (>= 3.0) @@ -741,6 +769,8 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-gitlab-security (0.0.6) + rubocop (>= 0.47.1) rubocop-rspec (1.15.1) rubocop (>= 0.42.0) ruby-fogbugz (0.2.1) @@ -881,6 +911,14 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + unparser (0.2.6) + abstract_type (~> 0.0.7) + adamantium (~> 0.2.0) + concord (~> 0.1.5) + diff-lcs (~> 1.3) + equalizer (~> 0.0.9) + parser (>= 2.3.1.2, < 2.5) + procto (~> 0.0.2) url_safe_base64 (0.2.2) validates_hostname (1.0.6) activerecord (>= 3.0) @@ -920,7 +958,6 @@ DEPENDENCIES activerecord_sane_schema_dumper (= 0.2) acts-as-taggable-on (~> 4.0) addressable (~> 2.3.8) - after_commit_queue (~> 1.3.0) akismet (~> 2.0) allocations (~> 1.0) asana (~> 0.6.0) @@ -943,7 +980,7 @@ DEPENDENCIES capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 1.1) - charlock_holmes (~> 0.7.3) + charlock_holmes (~> 0.7.5) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) concurrent-ruby (~> 1.0.5) @@ -982,19 +1019,19 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly (~> 0.26.0) + gitaly (~> 0.27.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) - gitlab_omniauth-ldap (~> 2.0.3) + gitlab_omniauth-ldap (~> 2.0.4) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) google-api-client (~> 0.8.6) gpgme - grape (~> 0.19.2) + grape (~> 1.0) grape-entity (~> 0.6.0) - grape-route-helpers (~> 2.0.0) + grape-route-helpers (~> 2.1.0) haml_lint (~> 0.26.0) hamlit (~> 2.6.1) hashie-forbidden_attributes @@ -1009,7 +1046,7 @@ DEPENDENCIES jquery-rails (~> 4.1.0) json-schema (~> 2.6.2) jwt (~> 1.5.6) - kaminari (~> 0.17.0) + kaminari (~> 1.0) knapsack (~> 1.11.0) kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) @@ -1083,11 +1120,13 @@ DEPENDENCIES responders (~> 2.0) rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) + rspec-parameterized rspec-rails (~> 3.6.0) rspec-retry (~> 0.4.5) rspec-set (~> 0.1.3) rspec_profiling (~> 0.0.5) rubocop (~> 0.49.1) + rubocop-gitlab-security (~> 0.0.6) rubocop-rspec (~> 1.15.1) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.16.2) diff --git a/PROCESS.md b/PROCESS.md index 2b3d142bf77..538e4389e00 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -119,6 +119,12 @@ only be left until after the freeze if: are aware of it. * It is in the correct milestone, with the ~Deliverable label. +If a merge request is not ready, but the developers and Product Manager +responsible for the feature think it is essential that it is in the release, +they can [ask for an exception](#asking-for-an-exception) in advance. This is +preferable to merging something that we are not confident in, but should still +be a rare case: most features can be allowed to slip a release. + All Community Edition merge requests from GitLab team members merged on the freeze date (the 7th) should have a corresponding Enterprise Edition merge request, even if there are no conflicts. This is to reduce the size of the @@ -128,11 +134,30 @@ information, see ### After the 7th -Once the stable branch is frozen, only fixes for [regressions](#regressions) -and security issues will be cherry-picked into the stable branch. -Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. -These fixes will be shipped in the next RC for that release if it is before the 22nd. -If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release. +Once the stable branch is frozen, the only MRs that can be cherry-picked into +the stable branch are: + +* Fixes for [regressions](#regressions) +* Fixes for security issues +* New or updated translations (as long as they do not touch application code) + +During the feature freeze all merge requests that are meant to go into the upcoming +release should have the correct milestone assigned _and_ have the label +~"Pick into Stable" set, so that release managers can find and pick them. +Merge requests without a milestone and this label will +not be merged into any stable branches. + +Fixes marked like this will be shipped in the next RC for that release. Once +the final RC has been prepared ready for release on the 22nd, further fixes +marked ~"Pick into Stable" will go into a patch for that release. + +If a merge request is to be picked into more than one release it will also need +the ~"Pick into Backports" label set to remind the release manager to change +the milestone after cherry-picking. As before, it should still have the +~"Pick into Stable" label and the milestone of the highest release it will be +picked into. + +### Asking for an exception If you think a merge request should go into an RC or patch even though it does not meet these requirements, you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer: @@ -152,11 +177,7 @@ When in doubt, we err on the side of _not_ cherry-picking. For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement (e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested. -During the feature freeze all merge requests that are meant to go into the upcoming -release should have the correct milestone assigned _and_ have the label -~"Pick into Stable" set, so that release managers can find and pick them. -Merge requests without a milestone and this label will -not be merged into any stable branches. +All MRs which have had exceptions granted must be merged by the 15th. ### Regressions diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 76b724e1bcb..56f91e95bb9 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -97,7 +97,6 @@ const Api = { }, commitMultiple(id, data, callback) { - // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const url = Api.buildUrl(Api.commitPath) .replace(':id', id); return $.ajax({ diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index dc636050221..26d3419a162 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,9 +1,24 @@ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* global Dropzone */ +import '../lib/utils/url_utility'; +import { HIDDEN_CLASS } from '../lib/utils/constants'; + +function toggleLoading($el, $icon, loading) { + if (loading) { + $el.disable(); + $icon.removeClass(HIDDEN_CLASS); + } else { + $el.enable(); + $icon.addClass(HIDDEN_CLASS); + } +} export default class BlobFileDropzone { constructor(form, method) { const formDropzone = form.find('.dropzone'); + const submitButton = form.find('#submit-all'); + const submitButtonLoadingIcon = submitButton.find('.js-loading-icon'); + const dropzoneMessage = form.find('.dz-message'); Dropzone.autoDiscover = false; const dropzone = formDropzone.dropzone({ @@ -26,12 +41,20 @@ export default class BlobFileDropzone { }, init: function () { this.on('addedfile', function () { + toggleLoading(submitButton, submitButtonLoadingIcon, false); + dropzoneMessage.addClass(HIDDEN_CLASS); $('.dropzone-alerts').html('').hide(); }); + this.on('removedfile', function () { + toggleLoading(submitButton, submitButtonLoadingIcon, false); + dropzoneMessage.removeClass(HIDDEN_CLASS); + }); this.on('success', function (header, response) { - window.location.href = response.filePath; + $('#modal-upload-blob').modal('hide'); + window.gl.utils.visitUrl(response.filePath); }); this.on('maxfilesexceeded', function (file) { + dropzoneMessage.addClass(HIDDEN_CLASS); this.removeFile(file); }); this.on('sending', function (file, xhr, formData) { @@ -48,14 +71,15 @@ export default class BlobFileDropzone { }, }); - const submitButton = form.find('#submit-all')[0]; - submitButton.addEventListener('click', function (e) { + submitButton.on('click', (e) => { e.preventDefault(); e.stopPropagation(); if (dropzone[0].dropzone.getQueuedFiles().length === 0) { // eslint-disable-next-line no-alert alert('Please select a file'); + return false; } + toggleLoading(submitButton, submitButtonLoadingIcon, true); dropzone[0].dropzone.processQueue(); return false; }); diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 363269c0d5d..b4a45feee4d 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -1,7 +1,7 @@ /* global ListIssue */ -/* global bp */ import Vue from 'vue'; +import bp from '../../../breakpoints'; const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index 2c1f988d987..7951348d8b2 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,66 +1,19 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */ +export const breakpoints = { + lg: 1200, + md: 992, + sm: 768, + xs: 0, +}; -var Breakpoints = (function() { - var BreakpointInstance, instance; +const BreakpointInstance = { + windowWidth: () => window.innerWidth, + getBreakpointSize() { + const windowWidth = this.windowWidth(); - function Breakpoints() {} + const breakpoint = Object.keys(breakpoints).find(key => windowWidth > breakpoints[key]); - instance = null; + return breakpoint; + }, +}; - BreakpointInstance = (function() { - var BREAKPOINTS; - - BREAKPOINTS = ["xs", "sm", "md", "lg"]; - - function BreakpointInstance() { - this.setup(); - } - - BreakpointInstance.prototype.setup = function() { - var allDeviceSelector, els; - allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { - return ".device-" + breakpoint; - }); - if ($(allDeviceSelector.join(",")).length) { - return; - } - // Create all the elements - els = $.map(BREAKPOINTS, function(breakpoint) { - return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; - }); - return $("body").append(els.join('')); - }; - - BreakpointInstance.prototype.visibleDevice = function() { - var allDeviceSelector; - allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { - return ".device-" + breakpoint; - }); - return $(allDeviceSelector.join(",")).filter(":visible"); - }; - - BreakpointInstance.prototype.getBreakpointSize = function() { - var $visibleDevice; - $visibleDevice = this.visibleDevice; - // TODO: Consider refactoring in light of turbolinks removal. - // the page refreshed via turbolinks - if (!$visibleDevice().length) { - this.setup(); - } - $visibleDevice = this.visibleDevice(); - return $visibleDevice.attr("class").split("visible-")[1]; - }; - - return BreakpointInstance; - })(); - - Breakpoints.get = function() { - return instance != null ? instance : instance = new BreakpointInstance; - }; - - return Breakpoints; -})(); - -$(() => { window.bp = Breakpoints.get(); }); - -window.Breakpoints = Breakpoints; +export default BreakpointInstance; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 940326dcd33..ae1a23132a7 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,8 +1,7 @@ /* eslint-disable func-names, wrap-iife, no-use-before-define, consistent-return, prefer-rest-params */ -/* global Breakpoints */ - import _ from 'underscore'; +import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; window.Build = (function () { @@ -34,8 +33,6 @@ window.Build = (function () { this.$scrollBottomBtn = $('.js-scroll-down'); clearTimeout(Build.timeout); - // Init breakpoint checker - this.bp = Breakpoints.get(); this.initSidebar(); this.populateJobs(this.buildStage); @@ -230,7 +227,7 @@ window.Build = (function () { }; Build.prototype.shouldHideSidebarForViewport = function () { - const bootstrapBreakpoint = this.bp.getBreakpointSize(); + const bootstrapBreakpoint = bp.getBreakpointSize(); return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; }; diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 2b0bf49cf92..047544b1762 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -17,7 +17,7 @@ window.CommitsList = (function() { } }); - Pager.init(limit, false, false, this.processCommits); + Pager.init(parseInt(limit, 10), false, false, this.processCommits); this.content = $("#commits-list"); this.searchField = $("#commits-search"); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index a2d33b0936e..5decfc1dc01 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -42,6 +42,10 @@ $(() => { $components.each(function () { const $this = $(this); const noteId = $this.attr(':note-id'); + const discussionId = $this.attr(':discussion-id'); + + if ($this.is('comment-and-resolve-btn') && !discussionId) return; + const tmp = Vue.extend({ template: $this.get(0).outerHTML }); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7cc7636cca3..a0ed5c23ffe 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -76,6 +76,7 @@ import initLegacyFilters from './init_legacy_filters'; import initIssuableSidebar from './init_issuable_sidebar'; import GpgBadges from './gpg_badges'; import UserFeatureHelper from './helpers/user_feature_helper'; +import initChangesDropdown from './init_changes_dropdown'; (function() { var Dispatcher; @@ -228,6 +229,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; break; case 'projects:compare:show': new gl.Diff(); + initChangesDropdown(); break; case 'projects:branches:new': case 'projects:branches:create': @@ -320,6 +322,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; container: '.js-commit-pipeline-graph', }).bindEvents(); initNotes(); + initChangesDropdown(); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:commit:pipelines': @@ -344,6 +347,9 @@ import UserFeatureHelper from './helpers/user_feature_helper'; if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); if ($('.project-show-activity').length) new gl.Activities(); + $('#tree-slider').waitForImages(function() { + gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + }); break; case 'projects:edit': setupProjectEdit(); @@ -638,7 +644,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; return Dispatcher; })(); - $(function() { + $(window).on('load', function() { new Dispatcher(); }); }).call(window); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 2856c8e2862..ee71728184f 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,7 +1,7 @@ /* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ /* global dateFormat */ -/* global Pikaday */ +import Pikaday from 'pikaday'; import DateFix from './lib/utils/datefix'; class DueDateSelect { diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 301e82f4610..32cb42c8b10 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -1,7 +1,54 @@ -/* global bp */ -import './breakpoints'; +import Cookies from 'js-cookie'; +import bp from './breakpoints'; -export const canShowSubItems = () => bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; +const HIDE_INTERVAL_TIMEOUT = 300; +const IS_OVER_CLASS = 'is-over'; +const IS_ABOVE_CLASS = 'is-above'; +const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out'; +let currentOpenMenu = null; +let menuCornerLocs; +let timeoutId; + +export const mousePos = []; + +export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; + +export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); + +let headerHeight = 50; + +export const getHeaderHeight = () => headerHeight; + +export const canShowActiveSubItems = (el) => { + const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; + + if (el.classList.contains('active') && !isHiddenByMedia) { + return Cookies.get('sidebar_collapsed') === 'true'; + } + + return true; +}; + +export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; + +export const getHideSubItemsInterval = () => { + if (!currentOpenMenu) return 0; + + const currentMousePos = mousePos[mousePos.length - 1]; + const prevMousePos = mousePos[0]; + const currentMousePosY = currentMousePos.y; + const [menuTop, menuBottom] = menuCornerLocs; + + if (currentMousePosY < menuTop.y || + currentMousePosY > menuBottom.y) return 0; + + if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && + slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { + return HIDE_INTERVAL_TIMEOUT; + } + + return 0; +}; export const calculateTop = (boundingRect, outerHeight) => { const windowHeight = window.innerHeight; @@ -11,42 +58,120 @@ export const calculateTop = (boundingRect, outerHeight) => { boundingRect.top; }; -export const showSubLevelItems = (el) => { - const subItems = el.querySelector('.sidebar-sub-level-items'); +export const hideMenu = (el) => { + if (!el) return; - if (!subItems || !canShowSubItems()) return; + const parentEl = el.parentNode; - subItems.style.display = 'block'; - el.classList.add('is-over'); + el.style.display = ''; // eslint-disable-line no-param-reassign + el.style.transform = ''; // eslint-disable-line no-param-reassign + el.classList.remove(IS_ABOVE_CLASS); + parentEl.classList.remove(IS_OVER_CLASS); + parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); + + setOpenMenu(); +}; +export const moveSubItemsToPosition = (el, subItems) => { const boundingRect = el.getBoundingClientRect(); const top = calculateTop(boundingRect, subItems.offsetHeight); const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; + subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign + + const subItemsRect = subItems.getBoundingClientRect(); + + menuCornerLocs = [ + { + x: subItemsRect.left, // left position of the sub items + y: subItemsRect.top, // top position of the sub items + }, + { + x: subItemsRect.left, // left position of the sub items + y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items + }, + ]; if (isAbove) { - subItems.classList.add('is-above'); + subItems.classList.add(IS_ABOVE_CLASS); } }; -export const hideSubLevelItems = (el) => { +export const showSubLevelItems = (el) => { + const subItems = el.querySelector('.sidebar-sub-level-items'); + + if (!canShowSubItems() || !canShowActiveSubItems(el)) return; + + el.classList.add(IS_OVER_CLASS); + + if (!subItems) return; + + subItems.style.display = 'block'; + el.classList.add(IS_SHOWING_FLY_OUT_CLASS); + + setOpenMenu(subItems); + moveSubItemsToPosition(el, subItems); +}; + +export const mouseEnterTopItems = (el) => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + if (currentOpenMenu) hideMenu(currentOpenMenu); + + showSubLevelItems(el); + }, getHideSubItemsInterval()); +}; + +export const mouseLeaveTopItem = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); - if (!subItems || !canShowSubItems()) return; + if (!canShowSubItems() || !canShowActiveSubItems(el) || + (subItems && subItems === currentOpenMenu)) return; - el.classList.remove('is-over'); - subItems.style.display = 'none'; - subItems.classList.remove('is-above'); + el.classList.remove(IS_OVER_CLASS); +}; + +export const documentMouseMove = (e) => { + mousePos.push({ + x: e.clientX, + y: e.clientY, + }); + + if (mousePos.length > 6) mousePos.shift(); }; export default () => { - const items = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active)')] - .filter(el => el.querySelector('.sidebar-sub-level-items')); + const sidebar = document.querySelector('.sidebar-top-level-items'); + + if (!sidebar) return; + + const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')]; + + sidebar.addEventListener('mouseleave', () => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + if (currentOpenMenu) hideMenu(currentOpenMenu); + }, getHideSubItemsInterval()); + }); + + headerHeight = document.querySelector('.nav-sidebar').offsetTop; items.forEach((el) => { - el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); - el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); + const subItems = el.querySelector('.sidebar-sub-level-items'); + + if (subItems) { + subItems.addEventListener('mouseleave', () => { + clearTimeout(timeoutId); + hideMenu(currentOpenMenu); + }); + } + + el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget)); + el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget)); }); + + document.addEventListener('mousemove', documentMouseMove); }; diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 1c379e9bb67..7ac9dcd1112 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,12 +1,14 @@ export default class GpgBadges { static fetch() { + const badges = $('.js-loading-gpg-badge'); const form = $('.commits-search-form'); + badges.html('<i class="fa fa-spinner fa-spin"></i>'); + $.get({ url: form.data('signatures-path'), data: form.serialize(), }).done((response) => { - const badges = $('.js-loading-gpg-badge'); response.signatures.forEach((signature) => { badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); }); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index b5975295329..4d629bc6326 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -111,8 +111,7 @@ window.GroupsSelect = (function() { }; GroupsSelect.prototype.forceOverflow = function (e) { - const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight; - this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`; + this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; }; GroupsSelect.PER_PAGE = 20; diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js new file mode 100644 index 00000000000..f785ed29e6c --- /dev/null +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -0,0 +1,10 @@ +import stickyMonitor from './lib/utils/sticky'; + +export default () => { + stickyMonitor(document.querySelector('.js-diff-files-changed')); + + $('.js-diff-stats-dropdown').glDropdown({ + filterable: true, + remoteFilter: false, + }); +}; diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 26392db4b5b..70c364e51fe 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,7 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ -/* global bp */ - import Cookies from 'js-cookie'; +import bp from './breakpoints'; import UsersSelect from './users_select'; const PARTICIPANTS_ROW_COUNT = 7; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 9ac1325fc95..3f848e0859b 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -2,8 +2,8 @@ /* global GitLab */ /* global Autosave */ /* global dateFormat */ -/* global Pikaday */ +import Pikaday from 'pikaday'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 5b9cf577189..3f6f40d47ba 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -40,7 +40,7 @@ label: 'New issue', path: this.job.new_issue_path, cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block', - type: 'ujs-link', + type: 'link', }); } diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index 1e96c7ab5cd..7a72509d234 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -1,2 +1,3 @@ /* eslint-disable import/prefer-default-export */ export const BYTES_IN_KIB = 1024; +export const HIDDEN_CLASS = 'hidden'; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 43a808b6ab3..ff2b66046b4 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,7 +1,7 @@ export const isSticky = (el, scrollY, stickyTop) => { const top = el.offsetTop - scrollY; - if (top === stickyTop) { + if (top <= stickyTop) { el.classList.add('is-stuck'); } else { el.classList.remove('is-stuck'); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 42092a34c2f..6d7c7e3c930 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ -/* global bp */ /* global Flash */ /* global ConfirmDangerModal */ /* global Aside */ @@ -7,7 +6,6 @@ import jQuery from 'jquery'; import _ from 'underscore'; import Cookies from 'js-cookie'; -import Pikaday from 'pikaday'; import Dropzone from 'dropzone'; import Sortable from 'vendor/Sortable'; @@ -20,7 +18,6 @@ import 'vendor/fuzzaldrin-plus'; window.jQuery = jQuery; window.$ = jQuery; window._ = _; -window.Pikaday = Pikaday; window.Dropzone = Dropzone; window.Sortable = Sortable; @@ -68,7 +65,7 @@ import './api'; import './aside'; import './autosave'; import loadAwardsHandler from './awards_handler'; -import './breakpoints'; +import bp from './breakpoints'; import './broadcast_message'; import './build'; import './build_artifacts'; @@ -135,8 +132,9 @@ import './project_select'; import './project_show'; import './project_variables'; import './projects_list'; -import './render_gfm'; +import './syntax_highlight'; import './render_math'; +import './render_gfm'; import './right_sidebar'; import './search'; import './search_autocomplete'; @@ -144,7 +142,6 @@ import './smart_interval'; import './star'; import './subscription'; import './subscription_select'; -import './syntax_highlight'; import './dispatcher'; diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index e034729bd39..cc9016e74da 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,5 +1,7 @@ -/* global Pikaday */ /* global dateFormat */ + +import Pikaday from 'pikaday'; + (() => { // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 4ffd71d9de5..5a9b3d19f84 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,13 +1,12 @@ /* eslint-disable no-new, class-methods-use-this */ -/* global Breakpoints */ /* global Flash */ /* global notes */ import Cookies from 'js-cookie'; -import './breakpoints'; import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; -import stickyMonitor from './lib/utils/sticky'; +import initChangesDropdown from './init_changes_dropdown'; +import bp from './breakpoints'; /* eslint-disable max-len */ // MergeRequestTabs @@ -134,7 +133,7 @@ import stickyMonitor from './lib/utils/sticky'; this.destroyPipelinesView(); } else if (this.isDiffAction(action)) { this.loadDiff($target.attr('href')); - if (Breakpoints.get().getBreakpointSize() !== 'lg') { + if (bp.getBreakpointSize() !== 'lg') { this.shrinkView(); } if (this.diffViewType() === 'parallel') { @@ -145,7 +144,7 @@ import stickyMonitor from './lib/utils/sticky'; this.resetViewContainer(); this.mountPipelinesView(); } else { - if (Breakpoints.get().getBreakpointSize() !== 'xs') { + if (bp.getBreakpointSize() !== 'xs') { this.expandView(); } this.resetViewContainer(); @@ -267,9 +266,7 @@ import stickyMonitor from './lib/utils/sticky'; const $container = $('#diffs'); $container.html(data.html); - this.initChangesDropdown(); - - stickyMonitor(document.querySelector('.js-diff-files-changed')); + initChangesDropdown(); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); @@ -319,13 +316,6 @@ import stickyMonitor from './lib/utils/sticky'; }); } - initChangesDropdown() { - $('.js-diff-stats-dropdown').glDropdown({ - filterable: true, - remoteFilter: false, - }); - } - // Show or hide the loading spinner // // status - Boolean, true to show, false to hide @@ -401,7 +391,7 @@ import stickyMonitor from './lib/utils/sticky'; // Screen space on small screens is usually very sparse // So we dont affix the tabs on these - if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return; /** If the browser does not support position sticky, it returns the position as static. diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue index c376baea79c..407af51cb7a 100644 --- a/app/assets/javascripts/monitoring/components/monitoring_column.vue +++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue @@ -1,5 +1,4 @@ <script> - /* global Breakpoints */ import d3 from 'd3'; import monitoringLegends from './monitoring_legends.vue'; import monitoringFlag from './monitoring_flag.vue'; @@ -8,6 +7,7 @@ import eventHub from '../event_hub'; import measurements from '../utils/measurements'; import { formatRelevantDigits } from '../../lib/utils/number_utils'; + import bp from '../../breakpoints'; const bisectDate = d3.bisector(d => d.time).left; @@ -42,7 +42,6 @@ yScale: {}, margin: {}, data: [], - breakpointHandler: Breakpoints.get(), unitOfDisplay: '', areaColorRgb: '#8fbce8', lineColorRgb: '#1f78d1', @@ -96,7 +95,7 @@ methods: { draw() { - const breakpointSize = this.breakpointHandler.getBreakpointSize(); + const breakpointSize = bp.getBreakpointSize(); const query = this.columnData.queries[0]; this.margin = measurements.large.margin; if (breakpointSize === 'xs' || breakpointSize === 'sm') { diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index 930218dd1f5..2d1ed9e4076 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -1,7 +1,6 @@ import Cookies from 'js-cookie'; import _ from 'underscore'; -/* global bp */ -import './breakpoints'; +import bp from './breakpoints'; export default class NewNavSidebar { constructor() { @@ -44,10 +43,12 @@ export default class NewNavSidebar { } toggleCollapsedSidebar(collapsed) { - this.$sidebar.toggleClass('sidebar-icons-only', collapsed); + const breakpoint = bp.getBreakpointSize(); + if (this.$sidebar.length) { + this.$sidebar.toggleClass('sidebar-icons-only', collapsed); this.$page.toggleClass('page-with-new-sidebar', !collapsed); - this.$page.toggleClass('page-with-icon-sidebar', collapsed); + this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); } diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js deleted file mode 100644 index c827b7402dc..00000000000 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue from 'vue'; -import Cookies from 'js-cookie'; -import Translate from '../../vue_shared/translate'; -import illustrationSvg from '../icons/intro_illustration.svg'; - -Vue.use(Translate); - -const cookieKey = 'pipeline_schedules_callout_dismissed'; - -export default { - name: 'PipelineSchedulesCallout', - data() { - return { - docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - illustrationSvg, - calloutDismissed: Cookies.get(cookieKey) === 'true', - }; - }, - methods: { - dismissCallout() { - this.calloutDismissed = true; - Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); - }, - }, - template: ` - <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout"> - <div class="bordered-box landing content-block"> - <button - id="dismiss-callout-btn" - class="btn btn-default close" - @click="dismissCallout"> - <i class="fa fa-times"></i> - </button> - <div class="svg-container" v-html="illustrationSvg"></div> - <div class="user-callout-copy"> - <h4>{{ __('Scheduling Pipelines') }}</h4> - <p> - {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} - </p> - <p> {{ __('Learn more in the') }} - <a - :href="docsUrl" - target="_blank" - rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> - </p> - </div> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue new file mode 100644 index 00000000000..6e0bc2d697a --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue @@ -0,0 +1,59 @@ +<script> + import Vue from 'vue'; + import Cookies from 'js-cookie'; + import Translate from '../../vue_shared/translate'; + import illustrationSvg from '../icons/intro_illustration.svg'; + + Vue.use(Translate); + + const cookieKey = 'pipeline_schedules_callout_dismissed'; + + export default { + name: 'PipelineSchedulesCallout', + data() { + return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + calloutDismissed: Cookies.get(cookieKey) === 'true', + }; + }, + methods: { + dismissCallout() { + this.calloutDismissed = true; + Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); + }, + }, + created() { + this.illustrationSvg = illustrationSvg; + }, + }; +</script> +<template> + <div + v-if="!calloutDismissed" + class="pipeline-schedules-user-callout user-callout"> + <div class="bordered-box landing content-block"> + <button + id="dismiss-callout-btn" + class="btn btn-default close" + @click="dismissCallout"> + <i + aria-hidden="true" + class="fa fa-times"> + </i> + </button> + <div class="svg-container" v-html="illustrationSvg"></div> + <div class="user-callout-copy"> + <h4>{{ __('Scheduling Pipelines') }}</h4> + <p> + {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} + </p> + <p> {{ __('Learn more in the') }} + <a + :href="docsUrl" + target="_blank" + rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> + </p> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js index 6584549ad06..a6c945e22b0 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; +import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipeline-schedules-callout', diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 2944689a5a7..7695b04db74 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -48,6 +48,27 @@ return `${this.job.name} - ${this.job.status.label}`; }, }, + + methods: { + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); + }, + }, + + mounted() { + this.stopDropdownClickPropagation(); + }, }; </script> <template> diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 1c2100a1c25..d7e3ab42f00 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -126,11 +126,11 @@ import Cookies from 'js-cookie'; var $form = $dropdown.closest('form'); var $visit = $dropdown.data('visit'); - var shouldVisit = typeof $visit === 'undefined' ? true : $visit; + var shouldVisit = $visit ? true : $visit; var action = $form.attr('action'); var divider = action.indexOf('?') === -1 ? '?' : '&'; if (shouldVisit) { - gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); + gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); } } } diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 985521aef34..7f972b6f6ee 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -36,7 +36,7 @@ const bindEvents = () => { $('.how_to_import_link').on('click', (e) => { e.preventDefault(); - $('.how_to_import_link').next('.modal').show(); + $(e.currentTarget).next('.modal').show(); }); $('.modal-header .close').on('click', () => { diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 2c3a9cacd38..bcdc0fd67b8 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -11,7 +11,5 @@ return this; }; - $(document).on('ready load', function() { - return $('body').renderGFM(); - }); + $(() => $('body').renderGFM()); }).call(window); diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 703da749ad3..d6c864cb976 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -14,13 +14,13 @@ export default { data: () => Store, mixins: [RepoMixin], components: { - 'repo-sidebar': RepoSidebar, - 'repo-tabs': RepoTabs, - 'repo-file-buttons': RepoFileButtons, + RepoSidebar, + RepoTabs, + RepoFileButtons, 'repo-editor': MonacoLoaderHelper.repoEditorLoader, - 'repo-commit-section': RepoCommitSection, - 'popup-dialog': PopupDialog, - 'repo-preview': RepoPreview, + RepoCommitSection, + PopupDialog, + RepoPreview, }, mounted() { @@ -28,12 +28,12 @@ export default { }, methods: { - dialogToggled(toggle) { + toggleDialogOpen(toggle) { this.dialog.open = toggle; }, dialogSubmitted(status) { - this.dialog.open = false; + this.toggleDialogOpen(false); this.dialog.status = status; }, @@ -43,21 +43,28 @@ export default { </script> <template> -<div class="repository-view tree-content-holder"> - <repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}"> - <repo-tabs/> - <component :is="currentBlobView" class="blob-viewer-container"></component> - <repo-file-buttons/> + <div class="repository-view"> + <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}"> + <repo-sidebar/> + <div v-if="isMini" + class="panel-right" + :class="{'edit-mode': editMode}"> + <repo-tabs/> + <component + :is="currentBlobView" + class="blob-viewer-container"/> + <repo-file-buttons/> + </div> + </div> + <repo-commit-section/> + <popup-dialog + v-show="dialog.open" + :primary-button-label="__('Discard changes')" + kind="warning" + :title="__('Are you sure?')" + :body="__('Are you sure you want to discard your changes?')" + @toggle="toggleDialogOpen" + @submit="dialogSubmitted" + /> </div> - <repo-commit-section/> - <popup-dialog - :primary-button-label="__('Discard changes')" - :open="dialog.open" - kind="warning" - :title="__('Are you sure?')" - :body="__('Are you sure you want to discard your changes?')" - @toggle="dialogToggled" - @submit="dialogSubmitted" - /> -</div> </template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index bd83f80c928..5ec4a9b6593 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -2,18 +2,20 @@ /* global Flash */ import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; -import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; -const RepoCommitSection = { +export default { data: () => Store, mixins: [RepoMixin], computed: { + showCommitable() { + return this.isCommitable && this.changedFiles.length; + }, + branchPaths() { - const branch = Helper.getBranch(); - return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch)); + return this.changedFiles.map(f => f.path); }, cantCommitYet() { @@ -28,11 +30,10 @@ const RepoCommitSection = { methods: { makeCommit() { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions - const branch = Helper.getBranch(); const commitMessage = this.commitMessage; const actions = this.changedFiles.map(f => ({ action: 'update', - file_path: Helper.getFilePathFromFullPath(f.url, branch), + file_path: f.path, content: f.newContent, })); const payload = { @@ -47,51 +48,80 @@ const RepoCommitSection = { resetCommitState() { this.submitCommitsLoading = false; this.changedFiles = []; - this.openedFiles = []; this.commitMessage = ''; this.editMode = false; - $('html, body').animate({ scrollTop: 0 }, 'fast'); + window.scrollTo(0, 0); }, }, }; - -export default RepoCommitSection; </script> <template> -<div id="commit-area" v-if="isCommitable && changedFiles.length" > - <form class="form-horizontal"> +<div + v-if="showCommitable" + id="commit-area"> + <form + class="form-horizontal" + @submit.prevent="makeCommit"> <fieldset> <div class="form-group"> - <label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label> - <div class="col-md-4"> + <label class="col-md-4 control-label staged-files"> + Staged files ({{changedFiles.length}}) + </label> + <div class="col-md-6"> <ul class="list-unstyled changed-files"> - <li v-for="file in branchPaths" :key="file.id"> - <span class="help-block">{{file}}</span> + <li + v-for="branchPath in branchPaths" + :key="branchPath"> + <span class="help-block"> + {{branchPath}} + </span> </li> </ul> </div> </div> - <!-- Textarea - --> <div class="form-group"> - <label class="col-md-4 control-label" for="commit-message">Commit message</label> - <div class="col-md-4"> - <textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea> + <label + class="col-md-4 control-label" + for="commit-message"> + Commit message + </label> + <div class="col-md-6"> + <textarea + id="commit-message" + class="form-control" + name="commit-message" + v-model="commitMessage"> + </textarea> </div> </div> - <!-- Button Drop Down - --> <div class="form-group target-branch"> - <label class="col-md-4 control-label" for="target-branch">Target branch</label> - <div class="col-md-4"> - <span class="help-block">{{targetBranch}}</span> + <label + class="col-md-4 control-label" + for="target-branch"> + Target branch + </label> + <div class="col-md-6"> + <span class="help-block"> + {{targetBranch}} + </span> </div> </div> - <div class="col-md-offset-4 col-md-4"> - <button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit"> - <i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i> - <span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span> + <div class="col-md-offset-4 col-md-6"> + <button + ref="submitCommit" + type="submit" + :disabled="cantCommitYet" + class="btn btn-success"> + <i + v-if="submitCommitsLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="loading"> + </i> + <span class="commit-summary"> + Commit {{changedFiles.length}} {{filePluralize}} + </span> </button> </div> </fieldset> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index e954fd38fc9..29b76975561 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -10,12 +10,15 @@ export default { return this.editMode ? this.__('Cancel edit') : this.__('Edit'); }, - buttonIcon() { - return this.editMode ? [] : ['fa', 'fa-pencil']; + showButton() { + return this.isCommitable && + !this.activeFile.render_error && + !this.binary && + this.openedFiles.length; }, }, methods: { - editClicked() { + editCancelClicked() { if (this.changedFiles.length) { this.dialog.open = true; return; @@ -23,27 +26,33 @@ export default { this.editMode = !this.editMode; Store.toggleBlobView(); }, + toggleProjectRefsForm() { + $('.project-refs-form').toggleClass('disabled', this.editMode); + $('.js-tree-ref-target-holder').toggle(this.editMode); + }, }, watch: { editMode() { - if (this.editMode) { - $('.project-refs-form').addClass('disabled'); - $('.fa-long-arrow-right').show(); - $('.project-refs-target-form').show(); - } else { - $('.project-refs-form').removeClass('disabled'); - $('.fa-long-arrow-right').hide(); - $('.project-refs-target-form').hide(); - } + this.toggleProjectRefsForm(); }, }, }; </script> <template> -<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary"> - <i :class="buttonIcon"></i> - <span>{{buttonLabel}}</span> +<button + v-if="showButton" + class="btn btn-default" + type="button" + @click.prevent="editCancelClicked"> + <i + v-if="!editMode" + class="fa fa-pencil" + aria-hidden="true"> + </i> + <span> + {{buttonLabel}} + </span> </button> </template> diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index fd1a21e15b4..96d6a75bb61 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -8,38 +8,39 @@ const RepoEditor = { data: () => Store, destroyed() { - // this.monacoInstance.getModels().forEach((m) => { - // m.dispose(); - // }); - this.monacoInstance.destroy(); + if (Helper.monacoInstance) { + Helper.monacoInstance.destroy(); + } }, mounted() { Service.getRaw(this.activeFile.raw_path) - .then((rawResponse) => { - Store.blobRaw = rawResponse.data; - Helper.findOpenedFileFromActive().plain = rawResponse.data; + .then((rawResponse) => { + Store.blobRaw = rawResponse.data; + Store.activeFile.plain = rawResponse.data; - const monacoInstance = this.monaco.editor.create(this.$el, { - model: null, - readOnly: false, - contextmenu: false, - }); + const monacoInstance = Helper.monaco.editor.create(this.$el, { + model: null, + readOnly: false, + contextmenu: false, + }); - Store.monacoInstance = monacoInstance; + Helper.monacoInstance = monacoInstance; - this.addMonacoEvents(); + this.addMonacoEvents(); - const languages = this.monaco.languages.getLanguages(); - const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); - this.showHide(); - const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); - - this.monacoInstance.setModel(newModel); - }).catch(Helper.loadingError); + this.setupEditor(); + }) + .catch(Helper.loadingError); }, methods: { + setupEditor() { + this.showHide(); + + Helper.setMonacoModelFromLanguage(); + }, + showHide() { if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { this.$el.style.display = 'none'; @@ -49,41 +50,36 @@ const RepoEditor = { }, addMonacoEvents() { - this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); - this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); + Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); + Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); }, onMonacoEditorKeysPressed() { - Store.setActiveFileContents(this.monacoInstance.getValue()); + Store.setActiveFileContents(Helper.monacoInstance.getValue()); }, onMonacoEditorMouseUp(e) { + if (!e.target.position) return; const lineNumber = e.target.position.lineNumber; - if (e.target.element.className === 'line-numbers') { + if (e.target.element.classList.contains('line-numbers')) { location.hash = `L${lineNumber}`; Store.activeLine = lineNumber; + + Helper.monacoInstance.setPosition({ + lineNumber: this.activeLine, + column: 1, + }); } }, }, watch: { - activeLine() { - this.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, - }); - }, - - activeFileLabel() { - this.showHide(); - }, - dialog: { handler(obj) { const newObj = obj; if (newObj.status) { newObj.status = false; - this.openedFiles.map((file) => { + this.openedFiles = this.openedFiles.map((file) => { const f = file; if (f.active) { this.blobRaw = f.plain; @@ -94,35 +90,21 @@ const RepoEditor = { return f; }); this.editMode = false; + Store.toggleBlobView(); } }, deep: true, }, - isTree() { - this.showHide(); - }, - - openedFiles() { - this.showHide(); - }, - - binary() { - this.showHide(); - }, - blobRaw() { - this.showHide(); - - if (this.isTree) return; - - this.monacoInstance.setModel(null); - - const languages = this.monaco.languages.getLanguages(); - const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); - const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); - - this.monacoInstance.setModel(newModel); + if (Helper.monacoInstance && !this.isTree) { + this.setupEditor(); + } + }, + }, + computed: { + shouldHideEditor() { + return !this.openedFiles.length || (this.binary && !this.activeFile.raw); }, }, }; @@ -131,5 +113,5 @@ export default RepoEditor; </script> <template> -<div id="ide"></div> +<div id="ide" v-if='!shouldHideEditor'></div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index f604bc22a26..20ebf840774 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -33,6 +33,26 @@ const RepoFile = { canShowFile() { return !this.loading.tree || this.hasFiles; }, + + fileIcon() { + const classObj = { + 'fa-spinner fa-spin': this.file.loading, + [this.file.icon]: !this.file.loading, + }; + return classObj; + }, + + fileIndentation() { + return { + 'margin-left': `${this.file.level * 10}px`, + }; + }, + + activeFileClass() { + return { + active: this.activeFile.url === this.file.url, + }; + }, }, methods: { @@ -46,21 +66,42 @@ export default RepoFile; </script> <template> -<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}"> - <td @click.prevent="linkClicked(file)"> - <i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i> - <i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i> - <a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a> +<tr + v-if="canShowFile" + class="file" + :class="activeFileClass" + @click.prevent="linkClicked(file)"> + <td> + <i + class="fa fa-fw file-icon" + :class="fileIcon" + :style="fileIndentation" + aria-label="file icon"> + </i> + <a + :href="file.url" + class="repo-file-name" + :title="file.url"> + {{file.name}} + </a> </td> - <td v-if="!isMini" class="hidden-sm hidden-xs"> - <div class="commit-message"> - <a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a> - </div> - </td> + <template v-if="!isMini"> + <td class="hidden-sm hidden-xs"> + <div class="commit-message"> + <a @click.stop :href="file.lastCommitUrl"> + {{file.lastCommitMessage}} + </a> + </div> + </td> - <td v-if="!isMini" class="hidden-xs"> - <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span> - </td> + <td class="hidden-xs"> + <span + class="commit-update" + :title="tooltipTitle(file.lastCommitUpdate)"> + {{timeFormated(file.lastCommitUpdate)}} + </span> + </td> + </template> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index 628d02ca704..e43ef366f47 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -15,7 +15,7 @@ const RepoFileButtons = { }, canPreview() { - return Helper.isKindaBinary(); + return Helper.isRenderable(); }, }, @@ -28,15 +28,42 @@ export default RepoFileButtons; </script> <template> -<div id="repo-file-buttons" v-if="isMini"> - <a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a> + <div id="repo-file-buttons"> + <a + :href="activeFile.raw_path" + target="_blank" + class="btn btn-default raw" + rel="noopener noreferrer"> + {{rawDownloadButtonLabel}} + </a> - <div class="btn-group" role="group" aria-label="File actions"> - <a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a> - <a :href="activeFile.commits_path" class="btn btn-default history">History</a> - <a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a> - </div> + <div + class="btn-group" + role="group" + aria-label="File actions"> + <a + :href="activeFile.blame_path" + class="btn btn-default blame"> + Blame + </a> + <a + :href="activeFile.commits_path" + class="btn btn-default history"> + History + </a> + <a + :href="activeFile.permalink" + class="btn btn-default permalink"> + Permalink + </a> + </div> - <a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a> -</div> + <a + v-if="canPreview" + href="#" + @click.prevent="rawPreviewToggle" + class="btn btn-default preview"> + {{activeFileLabel}} + </a> + </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue index ba53ce0eecc..6a15755f029 100644 --- a/app/assets/javascripts/repo/components/repo_file_options.vue +++ b/app/assets/javascripts/repo/components/repo_file_options.vue @@ -17,7 +17,7 @@ export default RepoFileOptions; </script> <template> -<tr v-if="isMini" class="repo-file-options"> + <tr v-if="isMini" class="repo-file-options"> <td> <span class="title">{{projectName}}</span> </td> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index 38e9f16d041..bc8c64c8362 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -18,9 +18,15 @@ const RepoLoadingFile = { }, }, + computed: { + showGhostLines() { + return this.loading.tree && !this.hasFiles; + }, + }, + methods: { lineOfCode(n) { - return `line-of-code-${n}`; + return `skeleton-line-${n}`; }, }, }; @@ -29,23 +35,42 @@ export default RepoLoadingFile; </script> <template> -<tr v-if="loading.tree && !hasFiles" class="loading-file"> - <td> - <div class="animation-container animation-container-small"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> + <tr + v-if="showGhostLines" + class="loading-file"> + <td> + <div + class="animation-container animation-container-small"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> - <td v-if="!isMini" class="hidden-sm hidden-xs"> - <div class="animation-container"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> + <td + v-if="!isMini" + class="hidden-sm hidden-xs"> + <div class="animation-container"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> - <td v-if="!isMini" class="hidden-xs"> - <div class="animation-container animation-container-small"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> -</tr> + <td + v-if="!isMini" + class="hidden-xs"> + <div class="animation-container animation-container-small"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> + </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index 6a0d684052f..bbdbdc61e38 100644 --- a/app/assets/javascripts/repo/components/repo_prev_directory.vue +++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue @@ -1,4 +1,6 @@ <script> +import RepoMixin from '../mixins/repo_mixin'; + const RepoPreviousDirectory = { props: { prevUrl: { @@ -7,6 +9,14 @@ const RepoPreviousDirectory = { }, }, + mixins: [RepoMixin], + + computed: { + colSpanCondition() { + return this.isMini ? undefined : 3; + }, + }, + methods: { linkClicked(file) { this.$emit('linkclicked', file); @@ -19,8 +29,10 @@ export default RepoPreviousDirectory; <template> <tr class="prev-directory"> - <td colspan="3"> - <a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a> + <td + :colspan="colSpanCondition" + @click.prevent="linkClicked(prevUrl)"> + <a :href="prevUrl">..</a> </td> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 81dc4523c8a..c6e914de54b 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -4,32 +4,39 @@ import Store from '../stores/repo_store'; export default { data: () => Store, + mounted() { - const $el = $(this.$el); + this.highlightFile(); - $el.find('.file-content').syntaxHighlight(); - $el + $(this.$el) .off('click', '.diff-line-num', Helper.diffLineNumClickWrapper) .on('click', '.diff-line-num', Helper.diffLineNumClickWrapper); Helper.highLightIfCurrentLine(); }, + computed: { html() { return this.activeFile.html; }, }, + methods: { + highlightFile() { + $(this.$el).find('.file-content').syntaxHighlight(); + }, + }, + watch: { html() { this.$nextTick(() => { - $(this.$el).find('.file-content').syntaxHighlight(); + this.highlightFile(); Helper.highLightIfCurrentLine(); }); }, activeFile: { - handler(obj) { + handler() { Helper.highLightIfCurrentLine(); }, deep: true, @@ -40,9 +47,23 @@ export default { <template> <div> - <div v-if="!activeFile.render_error" v-html="activeFile.html"></div> - <div v-if="activeFile.render_error" class="vertical-center render-error"> - <p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p> + <div + v-if="!activeFile.render_error" + v-html="activeFile.html"> + </div> + <div + v-else-if="activeFile.tooLarge" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead. + </p> + </div> + <div + v-else + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead. + </p> </div> </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index d6d832efc49..72b40288566 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue'; import RepoLoadingFile from './repo_loading_file.vue'; import RepoMixin from '../mixins/repo_mixin'; -const RepoSidebar = { +export default { mixins: [RepoMixin], components: { 'repo-file-options': RepoFileOptions, @@ -33,40 +33,36 @@ const RepoSidebar = { }); }, - linkClicked(clickedFile) { - let url = ''; + fileClicked(clickedFile) { let file = clickedFile; - if (typeof file === 'object') { - file.loading = true; - if (file.type === 'tree' && file.opened) { - file = Store.removeChildFilesOfTree(file); - file.loading = false; - } else { - url = file.url; - Service.url = url; - // I need to refactor this to do the `then` here. - // Not a callback. For now this is good enough. - // it works. - Helper.getContent(file, () => { + if (file.loading) return; + file.loading = true; + if (file.type === 'tree' && file.opened) { + file = Store.removeChildFilesOfTree(file); + file.loading = false; + } else { + Service.url = file.url; + Helper.getContent(file) + .then(() => { file.loading = false; Helper.scrollTabsRight(); - }); - } - } else if (typeof file === 'string') { - // go back - url = file; - Service.url = url; - Helper.getContent(null, () => Helper.scrollTabsRight()); + }) + .catch(Helper.loadingError); } }, + + goToPreviousDirectoryClicked(prevURL) { + Service.url = prevURL; + Helper.getContent(null) + .then(() => Helper.scrollTabsRight()) + .catch(Helper.loadingError); + }, }, }; - -export default RepoSidebar; </script> <template> -<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak> +<div id="sidebar" :class="{'sidebar-mini' : isMini}"> <table class="table"> <thead v-if="!isMini"> <tr> @@ -82,7 +78,7 @@ export default RepoSidebar; <repo-previous-directory v-if="isRoot" :prev-url="prevURL" - @linkclicked="linkClicked(prevURL)"/> + @linkclicked="goToPreviousDirectoryClicked(prevURL)"/> <repo-loading-file v-for="n in 5" :key="n" @@ -94,7 +90,7 @@ export default RepoSidebar; :key="file.id" :file="file" :is-mini="isMini" - @linkclicked="linkClicked(file)" + @linkclicked="fileClicked(file)" :is-tree="isTree" :has-files="!!files.length" :active-file="activeFile"/> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 712d64c236f..0d0c34ec741 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -10,10 +10,16 @@ const RepoTab = { }, computed: { + closeLabel() { + if (this.tab.changed) { + return `${this.tab.name} changed`; + } + return `Close ${this.tab.name}`; + }, changedClass() { const tabChangedObj = { - 'fa-times': !this.tab.changed, - 'fa-circle': this.tab.changed, + 'fa-times close-icon': !this.tab.changed, + 'fa-circle unsaved-icon': this.tab.changed, }; return tabChangedObj; }, @@ -22,9 +28,9 @@ const RepoTab = { methods: { tabClicked: Store.setActiveFiles, - xClicked(file) { + closeTab(file) { if (file.changed) return; - this.$emit('xclicked', file); + this.$emit('tabclosed', file); }, }, }; @@ -33,13 +39,25 @@ export default RepoTab; </script> <template> -<li> - <a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> - <i class="fa" :class="changedClass"></i> +<li @click="tabClicked(tab)"> + <a + href="#0" + class="close" + @click.stop.prevent="closeTab(tab)" + :aria-label="closeLabel"> + <i + class="fa" + :class="changedClass" + aria-hidden="true"> + </i> </a> - <a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> - - <i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> + <a + href="#" + class="repo-tab" + :title="tab.url" + @click.prevent="tabClicked(tab)"> + {{tab.name}} + </a> </li> </template> diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index 907a03e1601..9c5bfc5d0cf 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -1,5 +1,4 @@ <script> -import Vue from 'vue'; import Store from '../stores/repo_store'; import RepoTab from './repo_tab.vue'; import RepoMixin from '../mixins/repo_mixin'; @@ -14,30 +13,24 @@ const RepoTabs = { data: () => Store, methods: { - isOverflow() { - return this.$el.scrollWidth > this.$el.offsetWidth; - }, - - xClicked(file) { + tabClosed(file) { Store.removeFromOpenedFiles(file); }, }, - - watch: { - openedFiles() { - Vue.nextTick(() => { - this.tabsOverflow = this.isOverflow(); - }); - }, - }, }; export default RepoTabs; </script> <template> -<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}"> - <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> +<ul id="tabs"> + <repo-tab + v-for="tab in openedFiles" + :key="tab.id" + :tab="tab" + :class="{'active' : tab.active}" + @tabclosed="tabClosed" + /> <li class="tabs-divider" /> </ul> </template> diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js index 8ee2df5c879..f8729bbf585 100644 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js @@ -1,16 +1,20 @@ /* global monaco */ import RepoEditor from '../components/repo_editor.vue'; import Store from '../stores/repo_store'; +import Helper from '../helpers/repo_helper'; import monacoLoader from '../monaco_loader'; function repoEditorLoader() { Store.monacoLoading = true; return new Promise((resolve, reject) => { monacoLoader(['vs/editor/editor.main'], () => { - Store.monaco = monaco; + Helper.monaco = monaco; Store.monacoLoading = false; resolve(RepoEditor); - }, reject); + }, () => { + Store.monacoLoading = false; + reject(); + }); }); } diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index 040bcf05893..80cced14790 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -5,6 +5,8 @@ import Store from '../stores/repo_store'; import '../../flash'; const RepoHelper = { + monacoInstance: null, + getDefaultActiveFile() { return { active: true, @@ -35,19 +37,23 @@ const RepoHelper = { ? window.performance : Date, - getBranch() { - return $('button.dropdown-menu-toggle').attr('data-ref'); + getFileExtension(fileName) { + return fileName.split('.').pop(); }, getLanguageIDForFile(file, langs) { - const ext = file.name.split('.').pop(); + const ext = RepoHelper.getFileExtension(file.name); const foundLang = RepoHelper.findLanguage(ext, langs); return foundLang ? foundLang.id : 'plaintext'; }, - getFilePathFromFullPath(fullPath, branch) { - return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1]; + setMonacoModelFromLanguage() { + RepoHelper.monacoInstance.setModel(null); + const languages = RepoHelper.monaco.languages.getLanguages(); + const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages); + const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID); + RepoHelper.monacoInstance.setModel(newModel); }, findLanguage(ext, langs) { @@ -60,11 +66,11 @@ const RepoHelper = { file.opened = true; file.icon = 'fa-folder-open'; - RepoHelper.toURL(file.url, file.name); + RepoHelper.updateHistoryEntry(file.url, file.name); return file; }, - isKindaBinary() { + isRenderable() { const okExts = ['md', 'svg']; return okExts.indexOf(Store.activeFile.extension) > -1; }, @@ -78,22 +84,8 @@ const RepoHelper = { .catch(RepoHelper.loadingError); }, - toggleFakeTab(loading, file) { - if (loading) return Store.addPlaceholderFile(); - return Store.removeFromOpenedFiles(file); - }, - - setLoading(loading, file) { - if (Service.url.indexOf('blob') > -1) { - Store.loading.blob = loading; - return RepoHelper.toggleFakeTab(loading, file); - } - - if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading; - - return undefined; - }, - + // when you open a directory you need to put the directory files under + // the directory... This will merge the list of the current directory and the new list. getNewMergedList(inDirectory, currentList, newList) { const newListSorted = newList.sort(this.compareFilesCaseInsensitive); if (!inDirectory) return newListSorted; @@ -102,6 +94,9 @@ const RepoHelper = { return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); }, + // within the get new merged list this does the merging of the current list of files + // and the new list of files. The files are never "in" another directory they just + // appear like they are because of the margin. mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) { newList.reverse().forEach((newFile) => { const fileIndex = indexOfFile + 1; @@ -137,21 +132,17 @@ const RepoHelper = { return isRoot; }, - getContent(treeOrFile, cb) { + getContent(treeOrFile) { let file = treeOrFile; - // const loadingData = RepoHelper.setLoading(true); return Service.getContent() .then((response) => { const data = response.data; - // RepoHelper.setLoading(false, loadingData); - if (cb) cb(); Store.isTree = RepoHelper.isTree(data); if (!Store.isTree) { if (!file) file = data; Store.binary = data.binary; if (data.binary) { - Store.binaryMimeType = data.mime_type; // file might be undefined RepoHelper.setBinaryDataAsBase64(data); Store.setViewToPreview(); @@ -190,9 +181,8 @@ const RepoHelper = { setFile(data, file) { const newFile = data; - newFile.url = file.url || location.pathname; newFile.url = file.url; - if (newFile.render_error === 'too_large') { + if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { newFile.tooLarge = true; } newFile.newContent = ''; @@ -201,10 +191,6 @@ const RepoHelper = { Store.setActiveFiles(newFile); }, - toFA(icon) { - return `fa-${icon}`; - }, - serializeBlob(blob) { const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); simpleBlob.lastCommitMessage = blob.last_commit.message; @@ -228,7 +214,7 @@ const RepoHelper = { type, name, url, - icon: RepoHelper.toFA(icon), + icon: `fa-${icon}`, level: 0, loading: false, }; @@ -246,48 +232,30 @@ const RepoHelper = { setTimeout(() => { const tabs = document.getElementById('tabs'); if (!tabs) return; - tabs.scrollLeft = 12000; + tabs.scrollLeft = tabs.scrollWidth; }, 200); }, dataToListOfFiles(data) { - const a = []; - - // push in blobs - data.blobs.forEach((blob) => { - a.push(RepoHelper.serializeBlob(blob)); - }); - - data.trees.forEach((tree) => { - a.push(RepoHelper.serializeTree(tree)); - }); - - data.submodules.forEach((submodule) => { - a.push(RepoHelper.serializeSubmodule(submodule)); - }); - - return a; + const { blobs, trees, submodules } = data; + return [ + ...blobs.map(blob => RepoHelper.serializeBlob(blob)), + ...trees.map(tree => RepoHelper.serializeTree(tree)), + ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), + ]; }, genKey() { return RepoHelper.Time.now().toFixed(3); }, - getStateKey() { - return RepoHelper.key; - }, - - setStateKey(key) { - RepoHelper.key = key; - }, - - toURL(url, title) { + updateHistoryEntry(url, title) { const history = window.history; RepoHelper.key = RepoHelper.genKey(); history.pushState({ key: RepoHelper.key }, '', url); - if(window.location.hash) { + if (window.location.hash) { window.location.hash = window.location.hash; } diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 24c25e623a9..f7de3b8ce01 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue'; import Translate from '../vue_shared/translate'; function initDropdowns() { - $('.project-refs-target-form').hide(); - $('.fa-long-arrow-right').hide(); + $('.js-tree-ref-target-holder').hide(); } function addEventsForNonVueEls() { @@ -31,7 +30,7 @@ function setInitialStore(data) { Store.service = Service; Store.service.url = data.url; - if(window.location.hash) { + if (window.location.hash) { Store.hashToSet = window.location.hash; } @@ -39,6 +38,8 @@ function setInitialStore(data) { Store.projectId = data.projectId; Store.projectName = data.projectName; Store.projectUrl = data.projectUrl; + Store.canCommit = data.canCommit; + Store.onTopOfBranch = data.onTopOfBranch; Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.checkIsCommitable(); } @@ -49,6 +50,9 @@ function initRepo(el) { components: { repo: Repo, }, + render(createElement) { + return createElement('repo'); + }, }); } diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 8fba928e456..3cf204e6ec8 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -2,6 +2,7 @@ import axios from 'axios'; import Store from '../stores/repo_store'; import Api from '../../api'; +import Helper from '../helpers/repo_helper'; const RepoService = { url: '', @@ -12,16 +13,9 @@ const RepoService = { }, richExtensionRegExp: /md/, - checkCurrentBranchIsCommitable() { - const url = Store.service.refsUrl; - return axios.get(url, { params: { - ref: Store.currentBranch, - search: Store.currentBranch, - } }); - }, - getRaw(url) { return axios.get(url, { + // Stop Axios from parsing a JSON file into a JS object transformResponse: [res => res], }); }, @@ -36,7 +30,7 @@ const RepoService = { }, urlIsRichBlob(url = this.url) { - const extension = url.split('.').pop(); + const extension = Helper.getFileExtension(url); return this.richExtensionRegExp.test(extension); }, @@ -73,7 +67,11 @@ const RepoService = { commitFiles(payload, cb) { Api.commitMultiple(Store.projectId, payload, (data) => { - Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); + if (data.short_id && data.stats) { + Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); + } else { + Flash(data.message); + } cb(); }); }, diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 966caa0bdf2..32bfb2e9eac 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; const RepoStore = { - ideEl: {}, monaco: {}, monacoLoading: false, - monacoInstance: {}, service: '', - editor: '', - sidebar: '', + canCommit: false, + onTopOfBranch: false, editMode: false, isTree: false, isRoot: false, @@ -17,19 +15,10 @@ const RepoStore = { projectId: '', projectName: '', projectUrl: '', - trees: [], - blobs: [], - submodules: [], blobRaw: '', - blobRendered: '', currentBlobView: 'repo-preview', openedFiles: [], - tabSize: 100, - defaultTabSize: 100, - minTabSize: 30, - tabsOverflow: 41, submitCommitsLoading: false, - binaryLoaded: false, hashToSet: false, dialog: { open: false, @@ -46,9 +35,6 @@ const RepoStore = { currentBranch: '', targetBranch: 'new-branch', commitMessage: '', - binaryMimeType: '', - // scroll bar space for windows - scrollWidth: 0, binaryTypes: { png: false, md: false, @@ -59,7 +45,6 @@ const RepoStore = { tree: false, blob: false, }, - readOnly: true, resetBinaryTypes() { Object.keys(RepoStore.binaryTypes).forEach((key) => { @@ -69,14 +54,7 @@ const RepoStore = { // mutations checkIsCommitable() { - RepoStore.service.checkCurrentBranchIsCommitable() - .then((data) => { - // you shouldn't be able to make commits on commits or tags. - const { Branches, Commits, Tags } = data.data; - if (Branches && Branches.length) RepoStore.isCommitable = true; - if (Commits && Commits.length) RepoStore.isCommitable = false; - if (Tags && Tags.length) RepoStore.isCommitable = false; - }).catch(() => Flash('Failed to check if branch can be committed to.')); + RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; }, addFilesToDirectory(inDirectory, currentList, newList) { @@ -98,7 +76,6 @@ const RepoStore = { if (file.binary) { RepoStore.blobRaw = file.base64; - RepoStore.binaryMimeType = file.mime_type; } else if (file.newContent || file.plain) { RepoStore.blobRaw = file.newContent || file.plain; } else { @@ -109,11 +86,11 @@ const RepoStore = { }).catch(Helper.loadingError); } url = file.url; - if(file.currentLine) { + if (file.currentLine) { url = `${url}${file.currentLine}`; } - if (!file.loading) Helper.toURL(url, file.name); + if (!file.loading) Helper.updateHistoryEntry(url, file.name); RepoStore.binary = file.binary; }, @@ -146,15 +123,15 @@ const RepoStore = { removeChildFilesOfTree(tree) { let foundTree = false; const treeToClose = tree; - let wereDone = false; + let canStopSearching = false; RepoStore.files = RepoStore.files.filter((file) => { const isItTheTreeWeWant = file.url === treeToClose.url; // if it's the next tree if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { - wereDone = true; + canStopSearching = true; return true; } - if (wereDone) return true; + if (canStopSearching) return true; if (isItTheTreeWeWant) foundTree = true; @@ -171,8 +148,8 @@ const RepoStore = { if (file.type === 'tree') return; let foundIndex; RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { - if (openedFile.url === file.url) foundIndex = i; - return openedFile.url !== file.url; + if (openedFile.path === file.path) foundIndex = i; + return openedFile.path !== file.path; }); // now activate the right tab based on what you closed. @@ -186,36 +163,16 @@ const RepoStore = { return; } - if (foundIndex) { - if (foundIndex > 0) { - RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); - } + if (foundIndex && foundIndex > 0) { + RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); } }, - addPlaceholderFile() { - const randomURL = Helper.Time.now(); - const newFakeFile = { - active: false, - binary: true, - type: 'blob', - loading: true, - mime_type: 'loading', - name: 'loading', - url: randomURL, - fake: true, - }; - - RepoStore.openedFiles.push(newFakeFile); - - return newFakeFile; - }, - addToOpenedFiles(file) { const openFile = file; const openedFilesAlreadyExists = RepoStore.openedFiles - .some(openedFile => openedFile.url === openFile.url); + .some(openedFile => openedFile.path === openFile.path); if (openedFilesAlreadyExists) return; @@ -250,4 +207,5 @@ const RepoStore = { return RepoStore.currentBlobView === 'repo-preview'; }, }; + export default RepoStore; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 422c02c7b7e..8e7abdbffef 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -71,7 +71,7 @@ export default { /> <div v-if="!isConfidential" class="no-value confidential-value"> <i class="fa fa-eye is-not-confidential"></i> - None + Not confidential </div> <div v-else class="value confidential-value hide-collapsed"> <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js index df19d7305f8..2752fe2b911 100644 --- a/app/assets/javascripts/sidebar_height_manager.js +++ b/app/assets/javascripts/sidebar_height_manager.js @@ -1,8 +1,11 @@ import _ from 'underscore'; +import Cookies from 'js-cookie'; export default { init() { if (!this.initialized) { + if (Cookies.get('new_nav') === 'true' && $('.js-issuable-sidebar').length) return; + this.$window = $(window); this.$rightSidebar = $('.js-right-sidebar'); this.$navHeight = $('.navbar-gitlab').outerHeight() + diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index bdc059f4a03..d305bd6acdc 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -120,7 +120,7 @@ export default { </a> <a - v-if="action.type === 'ujs-link'" + v-else-if="action.type === 'ujs-link'" :href="action.path" data-method="post" rel="nofollow" @@ -129,7 +129,7 @@ export default { </a> <button - v-else="action.type === 'button'" + v-else-if="action.type === 'button'" @click="onClickAction(action)" :disabled="action.isLoading" :class="action.cssClass" diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 7d339c0e753..994b33bc1c9 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -1,31 +1,37 @@ <script> -const PopupDialog = { +export default { name: 'popup-dialog', props: { - open: Boolean, - title: String, - body: String, + title: { + type: String, + required: true, + }, + body: { + type: String, + required: true, + }, kind: { type: String, + required: false, default: 'primary', }, closeButtonLabel: { type: String, + required: false, default: 'Cancel', }, primaryButtonLabel: { type: String, - default: 'Save changes', + required: true, }, }, computed: { - typeOfClass() { - const className = `btn-${this.kind}`; - const returnObj = {}; - returnObj[className] = true; - return returnObj; + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; }, }, @@ -33,33 +39,45 @@ const PopupDialog = { close() { this.$emit('toggle', false); }, - - yesClick() { - this.$emit('submit', true); - }, - - noClick() { - this.$emit('submit', false); + emitSubmit(status) { + this.$emit('submit', status); }, }, }; - -export default PopupDialog; </script> + <template> -<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog"> +<div + class="modal popup-dialog" + role="dialog" + tabindex="-1"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <button type="button" + class="close" + @click="close" + aria-label="Close"> + <span aria-hidden="true">×</span> + </button> <h4 class="modal-title">{{this.title}}</h4> </div> <div class="modal-body"> <p>{{this.body}}</p> </div> <div class="modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button> - <button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button> + <button + type="button" + class="btn btn-default" + @click="emitSubmit(false)"> + {{closeButtonLabel}} + </button> + <button type="button" + class="btn" + :class="btnKindClass" + @click="emitSubmit(true)"> + {{primaryButtonLabel}} + </button> </div> </div> </div> diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js index 51ed2b4fd15..a0025ddb598 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,10 +1,7 @@ -/* global Breakpoints */ - -import './breakpoints'; +import bp from './breakpoints'; export default class Wikis { constructor() { - this.bp = Breakpoints.get(); this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarExpanded = false; @@ -41,15 +38,15 @@ export default class Wikis { this.renderSidebar(); } - sidebarCanCollapse() { - const bootstrapBreakpoint = this.bp.getBreakpointSize(); + static sidebarCanCollapse() { + const bootstrapBreakpoint = bp.getBreakpointSize(); return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; } renderSidebar() { if (!this.sidebarEl) return; const { classList } = this.sidebarEl; - if (this.sidebarExpanded || !this.sidebarCanCollapse()) { + if (this.sidebarExpanded || !Wikis.sidebarCanCollapse()) { if (!classList.contains('right-sidebar-expanded')) { classList.remove('right-sidebar-collapsed'); classList.add('right-sidebar-expanded'); diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 3cd7f81da47..667b73e150d 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -187,3 +187,81 @@ a { .fade-in-full { animation: fadeInFull $fade-in-duration 1; } + + +.animation-container { + background: $repo-editor-grey; + height: 40px; + overflow: hidden; + position: relative; + + &.animation-container-small { + height: 12px; + } + + &::before { + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: blockTextShine; + animation-timing-function: linear; + background-image: $repo-editor-linear-gradient; + background-repeat: no-repeat; + background-size: 800px 45px; + content: ' '; + display: block; + height: 100%; + position: relative; + } + + div { + background: $white-light; + height: 6px; + left: 0; + position: absolute; + right: 0; + } + + .skeleton-line-1 { + left: 0; + top: 8px; + } + + .skeleton-line-2 { + left: 150px; + top: 0; + height: 10px; + } + + .skeleton-line-3 { + left: 0; + top: 23px; + } + + .skeleton-line-4 { + left: 0; + top: 38px; + } + + .skeleton-line-5 { + left: 200px; + top: 28px; + height: 10px; + } + + .skeleton-line-6 { + top: 14px; + left: 230px; + height: 10px; + } +} + +@keyframes blockTextShine { + 0% { + transform: translateX(-468px); + } + + 100% { + transform: translateX(468px); + } +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5e374360359..293aa194528 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -372,6 +372,10 @@ table { background: $gl-success !important; } +.dz-message { + margin: 0; +} + .space-right { margin-right: 10px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 02e0ba74158..1bb04b59a2a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -725,9 +725,9 @@ } // TODO: change global style and remove mixin -@mixin new-style-dropdown { - .dropdown-menu, - .dropdown-menu-nav { +@mixin new-style-dropdown($selector: '') { + #{$selector}.dropdown-menu, + #{$selector}.dropdown-menu-nav { .divider { margin: 6px 0; } @@ -773,7 +773,7 @@ } } - .dropdown-menu-align-right { + #{$selector}.dropdown-menu-align-right { margin-top: 2px; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d9f92e93160..b677882eba4 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -26,7 +26,7 @@ header { &.navbar-gitlab { padding: 0 16px; - z-index: 2000; + z-index: 1000; margin-bottom: 0; min-height: $header-height; background-color: $gray-light; diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 71d5949b023..c63114f85b4 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -47,7 +47,7 @@ font-family: $monospace_font; display: block; font-size: $code_font_size !important; - line-height: 19px; + min-height: 19px; white-space: nowrap; i { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index bd0367f86dd..bd521028c44 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -117,10 +117,6 @@ body { margin-top: $header-height + $performance-bar-height; } -[v-cloak] { - display: none; -} - .vertical-center { min-height: 100vh; display: flex; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index fcd4c72b430..e3920b5d3d9 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -204,6 +204,16 @@ } } + div.avatar { + display: inline-flex; + justify-content: center; + align-items: center; + + .center { + line-height: 14px; + } + } + strong { color: $gl-text-color; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d386ac5ba9c..071f20fc457 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -161,6 +161,8 @@ } .nav-controls { + @include new-style-dropdown; + display: inline-block; float: right; text-align: right; diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index b666223b120..4c35e3a9c3c 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default; $pre-border-color: $border-color; $table-bg-accent: $gray-light; + +$zindex-popover: 900; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index bf5f124d142..96409b10b99 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -339,6 +339,8 @@ a > code { @extend .ref-name; } +@include new-style-dropdown('.git-revision-dropdown'); + /** * Apply Markdown typography * diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 795ee91af8b..3e2f23e6b2a 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -403,6 +403,7 @@ header.navbar-gitlab-new { } .breadcrumbs-extra { + display: flex; flex: 0 0 auto; margin-left: auto; } diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 76dccd2df56..840a4f07a34 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -97,18 +97,25 @@ $new-sidebar-collapsed-width: 50px; top: $header-height; bottom: 0; left: 0; - overflow: auto; background-color: $gray-normal; box-shadow: inset -2px 0 0 $border-color; + transform: translate3d(0, 0, 0); &.sidebar-icons-only { width: $new-sidebar-collapsed-width; - .nav-item-name, .badge, .project-title { display: none; } + + .nav-item-name { + display: none; + } + + .sidebar-top-level-items > li > a { + min-height: 44px; + } } &.nav-sidebar-expanded { @@ -172,6 +179,12 @@ $new-sidebar-collapsed-width: 50px; } } +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} + .with-performance-bar .nav-sidebar { top: $header-height + $performance-bar-height; } @@ -182,7 +195,7 @@ $new-sidebar-collapsed-width: 50px; > li { a { - padding: 8px 16px 8px 50px; + padding: 8px 16px 8px 40px; &:hover, &:focus { @@ -215,12 +228,15 @@ $new-sidebar-collapsed-width: 50px; &:hover { color: $gl-text-color; + + svg { + fill: $gl-text-color; + } } } - &:not(.active) { + &.is-showing-fly-out { > a { - margin-left: 1px; margin-right: 2px; } @@ -229,7 +245,7 @@ $new-sidebar-collapsed-width: 50px; position: fixed; top: 0; left: $new-sidebar-width; - width: 150px; + min-width: 150px; margin-top: -1px; padding: 8px 1px; background-color: $white-light; @@ -243,31 +259,20 @@ $new-sidebar-collapsed-width: 50px; position: absolute; top: -30px; bottom: -30px; - left: 0; + left: -10px; right: -30px; z-index: -1; } - &::after { - content: ""; - position: absolute; - top: 44px; - left: -30px; - right: 35px; - bottom: 0; - height: 100%; - max-height: 150px; - z-index: -1; - transform: skew(33deg); - } - &.is-above { margin-top: 1px; + } + + > .active { + box-shadow: none; - &::after { - top: auto; - bottom: 44px; - transform: skew(-30deg); + > a { + background-color: transparent; } } @@ -294,6 +299,7 @@ $new-sidebar-collapsed-width: 50px; > a { margin-left: 4px; + padding-left: 12px; } .badge { @@ -306,8 +312,7 @@ $new-sidebar-collapsed-width: 50px; } } - &:not(.active):hover > a, - > a:hover, + &.active > a:hover, &.is-over > a { background-color: $white-light; } @@ -354,7 +359,7 @@ $new-sidebar-collapsed-width: 50px; .sidebar-icons-only { .context-header { - height: 60px; + height: 61px; a { padding: 10px 4px; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index cd9f2d787c5..46fbfe5f91e 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -286,6 +286,10 @@ .gpg-status-box { + &:empty { + display: none; + } + &.valid { @include green-status-color; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index da77346d8b2..913a1a95dca 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -560,9 +560,13 @@ } .diff-files-changed { + .inline-parallel-buttons { + position: relative; + z-index: 1; + } + .commit-stat-summary { @include new-style-dropdown; - z-index: -1; @media (min-width: $screen-sm-min) { margin-left: -$gl-padding; @@ -574,10 +578,14 @@ @media (min-width: $screen-sm-min) { position: -webkit-sticky; position: sticky; - top: 84px; + top: 34px; background-color: $white-light; z-index: 190; + &.diff-files-changed-merge-request { + top: 84px; + } + + .files, + .alert { margin-top: 1px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b78db402c13..49839a9b528 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -8,13 +8,13 @@ .is-confidential { color: $orange-600; background-color: $orange-50; - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } .is-not-confidential { - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } @@ -35,7 +35,7 @@ .commit-box, .info-well, .commit-ci-menu, - .files-changed, + .files-changed-inner, .limited-header-width, .limited-width-notes { @extend .fixed-width-container; @@ -81,6 +81,7 @@ border: 1px solid $white-normal; padding: 5px; max-height: calc(100vh - 100px); + max-width: 100%; } .emoji-block { @@ -259,7 +260,7 @@ padding-top: 10px; } - &:not(.issue-boards-sidebar):not([data-signed-in]) { + &:not(.issue-boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) { .issuable-sidebar-header { display: none; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index c90642178fc..b4468d6d0a2 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -108,6 +108,7 @@ background-color: $orange-50; border-radius: $border-radius-default $border-radius-default 0 0; border: 1px solid $border-gray-normal; + border-bottom: none; padding: 3px 12px; margin: auto; align-items: center; @@ -132,22 +133,9 @@ } } -.not-confidential { - padding: 0; - border-top: none; -} - -.right-sidebar-expanded { - .md-area { - border-radius: 0; - border-top: none; - } -} - -.right-sidebar-collapsed { - .confidential-issue-warning { - border-bottom: none; - } +.confidential-issue-warning + .md-area { + border-top-left-radius: 0; + border-top-right-radius: 0; } .discussion-form { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2bb867052f6..0a194f3707f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -453,7 +453,10 @@ ul.notes { } .note-actions { + align-self: flex-start; flex-shrink: 0; + display: inline-flex; + align-items: center; // For PhantomJS that does not support flex float: right; margin-left: 10px; @@ -463,18 +466,12 @@ ul.notes { float: none; margin-left: 0; } - - .note-action-button { - margin-left: 8px; - } - - .more-actions-toggle { - margin-left: 2px; - } } .more-actions { - display: inline-block; + float: right; // phantomjs fallback + display: flex; + align-items: flex-end; .tooltip { white-space: nowrap; @@ -482,16 +479,10 @@ ul.notes { } .more-actions-toggle { - padding: 0; - &:hover .icon, &:focus .icon { color: $blue-600; } - - .icon { - padding: 0 6px; - } } .more-actions-dropdown { @@ -519,28 +510,42 @@ ul.notes { @include notes-media('max', $screen-md-max) { float: none; margin-left: 0; + } +} - .note-action-button { - margin-left: 0; - } +.note-actions-item { + margin-left: 15px; + display: flex; + align-items: center; + + &.more-actions { + // compensate for narrow icon + margin-left: 10px; } } .note-action-button { - display: inline; - line-height: 20px; + line-height: 1; + padding: 0; + min-width: 16px; + color: $gray-darkest; .fa { - color: $gray-darkest; position: relative; - font-size: 17px; + font-size: 16px; } + + svg { height: 16px; width: 16px; - fill: $gray-darkest; + top: 0; vertical-align: text-top; + + path { + fill: currentColor; + } } .award-control-icon-positive, @@ -613,10 +618,7 @@ ul.notes { .note-role { position: relative; - top: -2px; - display: inline-block; - padding-left: 7px; - padding-right: 7px; + padding: 0 7px; color: $notes-role-color; font-size: 12px; line-height: 20px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 6185342b495..85d1905ad40 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -824,6 +824,7 @@ button.mini-pipeline-graph-dropdown-toggle { * Top arrow in the dropdown in the mini pipeline graph */ .mini-pipeline-graph-dropdown-menu { + z-index: 200; &::before, &::after { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 276465488e7..d01326637ea 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -566,14 +566,14 @@ a.deploy-project-label { &::before { content: "OR"; position: absolute; - left: 0; - top: 40%; + left: -10px; + top: 50%; z-index: 10; padding: 8px 0; text-align: center; background-color: $white-light; color: $gl-text-color-tertiary; - transform: translateX(-50%); + transform: translateY(-50%); font-size: 12px; font-weight: bold; line-height: 20px; @@ -581,8 +581,8 @@ a.deploy-project-label { // Mobile @media (max-width: $screen-xs-max) { left: 50%; - top: 10px; - transform: translateY(-50%); + top: 0; + transform: translateX(-50%); padding: 0 8px; } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index ad17078c98a..1f4d4698199 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,6 +1,6 @@ .fade-enter-active, .fade-leave-active { - transition: opacity .5s; + transition: opacity $sidebar-transition-duration; } .monaco-loader { @@ -28,11 +28,6 @@ .project-refs-form, .project-refs-target-form { display: inline-block; - - &.disabled { - opacity: 0.5; - pointer-events: none; - } } .fade-enter, @@ -52,14 +47,26 @@ margin: 20px; } -.repository-view.tree-content-holder { +.repository-view { border: 1px solid $border-color; border-radius: $border-radius-default; color: $almost-black; + .tree-content-holder { + display: flex; + max-height: 100vh; + min-height: 300px; + } + + .tree-content-holder-mini { + height: 100vh; + } + .panel-right { - display: inline-block; + display: flex; + flex-direction: column; width: 80%; + height: 100%; .monaco-editor.vs { .line-numbers { @@ -90,16 +97,17 @@ } .blob-viewer-container { - height: calc(100vh - 63px); + flex: 1; overflow: auto; } #tabs { + flex-shrink: 0; + display: flex; + width: 100%; padding-left: 0; margin-bottom: 0; - display: flex; white-space: nowrap; - width: 100%; overflow-y: hidden; overflow-x: auto; @@ -114,6 +122,7 @@ border-right: 1px solid $white-dark; border-bottom: 1px solid $white-dark; white-space: nowrap; + cursor: pointer; &.remove { animation: swipeRightDissapear ease-in 0.1s; @@ -133,10 +142,10 @@ a { @include str-truncated(100px); color: $black; - display: inline-block; width: 100px; text-align: center; vertical-align: middle; + text-decoration: none; &.close { width: auto; @@ -146,15 +155,15 @@ } } - i.fa.fa-times, - i.fa.fa-circle { + .close-icon, + .unsaved-icon { float: right; margin-top: 3px; margin-left: 15px; color: $gray-darkest; } - i.fa.fa-circle { + .unsaved-icon { color: $brand-success; } @@ -204,7 +213,7 @@ background: $gray-light; padding: 20px; - span.help-block { + .help-block { padding-top: 7px; margin-top: 0; } @@ -226,13 +235,12 @@ } #sidebar { + flex: 1; + height: 100%; &.sidebar-mini { - display: inline-block; - vertical-align: top; width: 20%; border-right: 1px solid $white-normal; - height: calc(100vh + 20px); overflow: auto; } @@ -261,7 +269,6 @@ text-transform: uppercase; font-weight: bold; color: $gray-darkest; - width: 185px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -270,7 +277,7 @@ } } - .fa { + .file-icon { margin-right: 5px; } @@ -280,118 +287,22 @@ } a { + @include str-truncated(250px); color: $almost-black; display: inline-block; vertical-align: middle; } - - ul { - list-style-type: none; - padding: 0; - - li { - border-bottom: 1px solid $border-gray-normal; - padding: 10px 20px; - - a { - color: $almost-black; - } - - .fa { - font-size: $code_font_size; - margin-right: 5px; - } - } - } - } - -} - -.animation-container { - background: $repo-editor-grey; - height: 40px; - overflow: hidden; - position: relative; - - &.animation-container-small { - height: 12px; - } - - &::before { - animation-duration: 1s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-name: blockTextShine; - animation-timing-function: linear; - background-image: $repo-editor-linear-gradient; - background-repeat: no-repeat; - background-size: 800px 45px; - content: ' '; - display: block; - height: 100%; - position: relative; - } - - div { - background: $white-light; - height: 6px; - left: 0; - position: absolute; - right: 0; - } - - .line-of-code-1 { - left: 0; - top: 8px; - } - - .line-of-code-2 { - left: 150px; - top: 0; - height: 10px; - } - - .line-of-code-3 { - left: 0; - top: 23px; - } - - .line-of-code-4 { - left: 0; - top: 38px; - } - - .line-of-code-5 { - left: 200px; - top: 28px; - height: 10px; - } - - .line-of-code-6 { - top: 14px; - left: 230px; - height: 10px; } } .render-error { - min-height: calc(100vh - 63px); + min-height: calc(100vh - 62px); p { width: 100%; } } -@keyframes blockTextShine { - 0% { - transform: translateX(-468px); - } - - 100% { - transform: translateX(468px); - } -} - @keyframes swipeRightAppear { 0% { transform: scaleX(0.00); diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index a8e0f251cd3..0028e207f3e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -29,6 +29,10 @@ margin-right: 15px; } + .tree-ref-target-holder { + display: inline-block; + } + .repo-breadcrumb { li:last-of-type { position: relative; @@ -216,6 +220,9 @@ } .blob-upload-dropzone-previews { + display: flex; + justify-content: center; + align-items: center; text-align: center; border: 2px; border-style: dashed; diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index 4b0ec54b3f4..92df1c8dff0 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -45,7 +45,7 @@ class Admin::AppearancesController < Admin::ApplicationController # Use callbacks to share common setup or constraints between actions. def set_appearance - @appearance = Appearance.last || Appearance.new + @appearance = Appearance.current || Appearance.new end # Only allow a trusted parameter "white list" through. diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5b448008a1b..1d92ea11bda 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base Raven.capture_exception(exception) if sentry_enabled? application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace - application_trace.map!{ |t| " #{t}\n" } + application_trace.map! { |t| " #{t}\n" } logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" end diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index ea441b1736b..b75e401a8df 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -69,7 +69,7 @@ module AuthenticatesWithTwoFactor if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) # Remove any lingering user data from login session.delete(:otp_user_id) - session.delete(:challenges) + session.delete(:challenge) remember_me(user) if user_params[:remember_me] == '1' sign_in(user) diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 52e06f4945a..1ab107168c0 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -6,6 +6,13 @@ module CycleAnalyticsParams end def start_date(params) - params[:start_date] == '30' ? 30.days.ago : 90.days.ago + case params[:start_date] + when '7' + 7.days.ago + when '30' + 30.days.ago + else + 90.days.ago + end end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0c3b68a7ac3..4079072a930 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -10,7 +10,7 @@ module IssuableActions def destroy issuable.destroy destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym - TodoService.new.public_send(destroy_method, issuable, current_user) + TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend name = issuable.human_class_name flash[:notice] = "The #{name} was successfully deleted." diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 74fe45e1ff6..f71ab702e71 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -52,8 +52,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def load_events - @events = Event.in_projects(load_projects(params.merge(non_public: true))) - @events = event_filter.apply_filter(@events).with_associations - @events = @events.limit(20).offset(params[:offset] || 0) + projects = load_projects(params.merge(non_public: true)) + + @events = EventCollection + .new(projects, offset: params[:offset].to_i, filter: event_filter) + .to_a end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index f9c31920302..19a5db6fd17 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -29,9 +29,9 @@ class DashboardController < Dashboard::ApplicationController current_user.authorized_projects end - @events = Event.in_projects(projects) - @events = @event_filter.apply_filter(@events).with_associations - @events = @events.limit(20).offset(params[:offset] || 0) + @events = EventCollection + .new(projects, offset: params[:offset].to_i, filter: @event_filter) + .to_a end def set_show_full_reference diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 741879dee35..762c6ebf3a3 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController def index params[:sort] ||= 'latest_activity_desc' @sort = params[:sort] - @projects = load_projects.page(params[:page]) + @projects = load_projects respond_to do |format| format.html @@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController def trending params[:trending] = true @sort = params[:sort] - @projects = load_projects.page(params[:page]) + @projects = load_projects respond_to do |format| format.html @@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def starred - @projects = load_projects.reorder('star_count DESC').page(params[:page]) + @projects = load_projects.reorder('star_count DESC') respond_to do |format| format.html @@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController def load_projects ProjectsFinder.new(current_user: current_user, params: params) - .execute.includes(:route, namespace: :route) + .execute + .includes(:route, namespace: :route) + .page(params[:page]) + .without_count end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index c0ac47e363d..96ce686c989 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -34,7 +34,7 @@ class Groups::ApplicationController < ApplicationController 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 27137ffde54..f76b3f69e9e 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -160,9 +160,9 @@ class GroupsController < Groups::ApplicationController end def load_events - @events = Event.in_projects(@projects) - @events = event_filter.apply_filter(@events).with_associations - @events = @events.limit(20).offset(params[:offset] || 0) + @events = EventCollection + .new(@projects, offset: params[:offset].to_i, filter: event_filter) + .to_a end def user_actions diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 53a5981e564..ab18d86dcae 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -64,19 +64,19 @@ class Import::GithubController < Import::BaseController end def import_enabled? - __send__("#{provider}_import_enabled?") + __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend end def new_import_url - public_send("new_import_#{provider}_url") + public_send("new_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend end def status_import_url - public_send("status_import_#{provider}_url") + public_send("status_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend end def callback_import_url - public_send("callback_import_#{provider}_url") + public_send("callback_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend end def provider_unauthorized diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 73837ffbe67..407154e59a0 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -15,7 +15,7 @@ class Import::GitlabController < Import::BaseController @already_added_projects = current_user.created_projects.where(import_type: "gitlab") already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos = @repos.to_a.reject{ |repo| already_added_projects_names.include? repo["path_with_namespace"] } + @repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] } end def jobs diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index b4213574561..7444826a5d1 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -142,13 +142,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def oauth @oauth ||= request.env['omniauth.auth'] end - + def fail_login error_message = @user.errors.full_messages.to_sentence return redirect_to omniauth_error_path(oauth['provider'], error: error_message) end - + def fail_ldap_login flash[:alert] = 'Access denied for your LDAP account.' diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index a2e8c10857d..2b8f3977e6e 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController json = blob_json(@blob) return render_404 unless json + path_segments = @path.split('/') + path_segments.pop + tree_path = path_segments.join('/') + render json: json.merge( path: blob.path, name: blob.name, @@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController raw_path: project_raw_path(project, @id), blame_path: project_blame_path(project, @id), commits_path: project_commits_path(project, @id), + tree_path: project_tree_path(project, File.join(@ref, tree_path)), permalink: project_blob_path(project, File.join(@commit.id, @path)) ) end diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index b69d46f2c41..26f3c114108 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -2,7 +2,7 @@ module Projects module CycleAnalytics class EventsController < Projects::ApplicationController include CycleAnalyticsParams - + before_action :authorize_read_cycle_analytics! before_action :authorize_read_build!, only: [:test, :staging] before_action :authorize_read_issue!, only: [:issue, :production] @@ -11,33 +11,33 @@ module Projects def issue render_events(cycle_analytics[:issue].events) end - + def plan render_events(cycle_analytics[:plan].events) end - + def code render_events(cycle_analytics[:code].events) end - + def test options(events_params)[:branch] = events_params[:branch_name] - + render_events(cycle_analytics[:test].events) end - + def review render_events(cycle_analytics[:review].events) end - + def staging render_events(cycle_analytics[:staging].events) end - + def production render_events(cycle_analytics[:production].events) end - + private def render_events(events) @@ -46,14 +46,14 @@ module Projects format.json { render json: { events: events } } end end - + def cycle_analytics @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end - + def events_params return {} unless params[:events].present? - + params[:events].permit(:start_date, :branch_name) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e2ccabb22db..8893a514207 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController end def create_merge_request - result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute + result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute if result[:status] == :success render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) @@ -257,18 +257,6 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless @project.feature_available?(:issues, current_user) end - def redirect_to_external_issue_tracker - external = @project.external_issue_tracker - - return unless external - - if action_name == 'new' - redirect_to external.new_issue_path - else - redirect_to external.issue_tracker_path - end - end - def issue_params params.require(:issue).permit(*issue_params_attributes) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4de814d0ca8..2a3b73577a5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,8 +218,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo if can?(current_user, :read_environment, environment) && environment.has_metrics? metrics_project_environment_deployment_path(environment.project, environment, deployment) end - - metrics_monitoring_url = + + metrics_monitoring_url = if can?(current_user, :read_environment, environment) environment_metrics_path(environment) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8dfe0f51709..1d24563a6a6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -7,6 +7,7 @@ class ProjectsController < Projects::ApplicationController before_action :repository, except: [:index, :new, :create] before_action :assign_ref_vars, only: [:show], if: :repo_exists? before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] + before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export] # Authorize before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] @@ -301,10 +302,11 @@ class ProjectsController < Projects::ApplicationController end def load_events - @events = @project.events.recent - @events = event_filter.apply_filter(@events).with_associations - limit = (params[:limit] || 20).to_i - @events = @events.limit(limit).offset(params[:offset] || 0) + projects = Project.where(id: @project.id) + + @events = EventCollection + .new(projects, offset: params[:offset].to_i, filter: event_filter) + .to_a end def project_params @@ -389,4 +391,8 @@ class ProjectsController < Projects::ApplicationController url_for(params) end + + def project_export_enabled + render_404 unless current_application_settings.project_export_enabled? + end end diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb index b7a1a046be0..ed04bd1f77d 100644 --- a/app/controllers/unicorn_test_controller.rb +++ b/app/controllers/unicorn_test_controller.rb @@ -1,12 +1,14 @@ +# :nocov: if Rails.env.test? class UnicornTestController < ActionController::Base def pid render plain: Process.pid.to_s end - + def kill Process.kill(params[:signal], Process.pid) render plain: 'Bye!' end end end +# :nocov: diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index dc882b17143..16a74f82d3f 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -89,7 +89,7 @@ class UploadsController < ApplicationController @uploader.retrieve_from_store!(params[:filename]) else - @uploader = @model.send(upload_mount) + @uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend redirect_to @uploader.url unless @uploader.file_storage? end diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb index a5ba791a513..7176bfe22d6 100644 --- a/app/finders/admin/projects_finder.rb +++ b/app/finders/admin/projects_finder.rb @@ -18,7 +18,7 @@ class Admin::ProjectsFinder end def execute - items = Project.with_statistics + items = Project.without_deleted.with_statistics items = items.in_namespace(namespace_id) if namespace_id.present? items = items.where(visibility_level: visibility_level) if visibility_level.present? items = items.with_push if with_push.present? diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 16136d02530..cdf5fa5d4b7 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -20,7 +20,7 @@ module AppearancesHelper end def brand_item - @appearance ||= Appearance.first + @appearance ||= Appearance.current end def brand_header_logo diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 6825adcb39f..150188f0b65 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -146,6 +146,7 @@ module ApplicationSettingsHelper :plantuml_enabled, :plantuml_url, :polling_interval_multiplier, + :project_export_enabled, :prometheus_metrics_enabled, :recaptcha_enabled, :recaptcha_private_key, diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 69220a1c0f6..72e26b64e60 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,10 +128,10 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) - user = commit.send(options[:source]) + user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend - source_name = clean(commit.send "#{options[:source]}_name".to_sym) - source_email = clean(commit.send "#{options[:source]}_email".to_sym) + source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend + source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend person_name = user.try(:name) || source_name diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 2e9b72e9613..c53ea4519da 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -3,7 +3,7 @@ module GraphHelper refs = "" # Commit::ref_names already strips the refs/XXX from important refs (e.g. refs/heads/XXX) # so anything leftover is internally used by GitLab - commit_refs = commit.ref_names(repo).reject{ |name| name.starts_with?('refs/') } + commit_refs = commit.ref_names(repo).reject { |name| name.starts_with?('refs/') } refs << commit_refs.join(' ') # append note count diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 8cd61f738e1..4123a96911f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -59,7 +59,7 @@ module GroupsHelper end def remove_group_message(group) - _("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % + _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") % { group_name: group.name } end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a57b5a8fea5..a18ebfb6030 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -5,7 +5,7 @@ module ImportHelper end def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index f4fad7150e8..197c90c4081 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -151,7 +151,7 @@ module IssuablesHelper end def issuable_labels_tooltip(labels, limit: 5) - first, last = labels.partition.with_index{ |_, i| i < limit } + first, last = labels.partition.with_index { |_, i| i < limit } label_names = first.collect(&:name) label_names << "and #{last.size} more" unless last.empty? @@ -174,7 +174,14 @@ module IssuablesHelper end def assigned_issuables_count(issuable_type) - current_user.public_send("assigned_open_#{issuable_type}_count") + case issuable_type + when :issues + current_user.assigned_open_issues_count + when :merge_requests + current_user.assigned_open_merge_requests_count + else + raise ArgumentError, "invalid issuable `#{issuable_type}`" + end end def issuable_filter_params @@ -234,7 +241,7 @@ module IssuablesHelper end def issuables_count_for_state(issuable_type, state, finder: nil) - finder ||= public_send("#{issuable_type}_finder") + finder ||= public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend cache_key = finder.state_counter_cache_key @counts ||= {} @@ -298,10 +305,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def base_issuable_scope(issuable) - issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) - end - def issuable_state_scope(issuable) if issuable.respond_to?(:merged?) && issuable.merged? :merged @@ -329,7 +332,7 @@ module IssuablesHelper end def selected_template(issuable) - params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] } + params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } end def issuable_todo_button_data(issuable, todo, is_collapsed) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4b99de1b6a5..e60513b35c7 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -43,11 +43,11 @@ module LabelsHelper def label_filter_path(subject, label, type: :issue) case subject when Group - send("#{type.to_s.pluralize}_group_path", + send("#{type.to_s.pluralize}_group_path", # rubocop:disable GitlabSecurity/PublicSend subject, label_name: [label.name]) when Project - send("namespace_project_#{type.to_s.pluralize}_path", + send("namespace_project_#{type.to_s.pluralize}_path", # rubocop:disable GitlabSecurity/PublicSend subject.namespace, subject, label_name: [label.name]) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index f8860bfee99..86666022a2a 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -32,7 +32,18 @@ module MilestonesHelper end def milestone_issues_by_label_count(milestone, label, state:) - milestone.issues.with_label(label.title).send(state).size + issues = milestone.issues.with_label(label.title) + issues = + case state + when :opened + issues.opened + when :closed + issues.closed + else + raise ArgumentError, "invalid milestone state `#{state}`" + end + + issues.size end # Returns count of milestones for different states diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb new file mode 100644 index 00000000000..83dd76a01dd --- /dev/null +++ b/app/helpers/pagination_helper.rb @@ -0,0 +1,21 @@ +module PaginationHelper + def paginate_collection(collection, remote: nil) + if collection.is_a?(Kaminari::PaginatableWithoutCount) + paginate_without_count(collection) + elsif collection.respond_to?(:total_pages) + paginate_with_count(collection, remote: remote) + end + end + + def paginate_without_count(collection) + render( + 'kaminari/gitlab/without_count', + previous_path: path_to_prev_page(collection), + next_path: path_to_next_page(collection) + ) + end + + def paginate_with_count(collection, remote: nil) + paginate(collection, remote: remote, theme: 'gitlab') + end +end diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index fee1edc2a1b..6edaf78de1b 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -1,10 +1,10 @@ module PipelineSchedulesHelper def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.utc_offset, - identifier: timezone.tzinfo.identifier + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.utc_offset, + identifier: timezone.tzinfo.identifier } end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a268413e84f..bee4950e414 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,7 +80,7 @@ module ProjectsHelper end def remove_project_message(project) - _("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % + _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % { project_name_with_namespace: project.name_with_namespace } end @@ -149,15 +149,16 @@ module ProjectsHelper # Don't show option "everyone with access" if project is private options = project_feature_options + level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend + if @project.private? - level = @project.project_feature.send(field) disabled_option = ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end options = options_for_select( options.invert, - selected: highest_available_option || @project.project_feature.public_send(field), + selected: highest_available_option || level, disabled: disabled_option ) @@ -234,6 +235,8 @@ module ProjectsHelper # If no limit is applied we'll just issue a COUNT since the result set could # be too large to load into memory. def any_projects?(projects) + return projects.any? if projects.is_a?(Array) + if projects.limit_value projects.to_a.any? else @@ -486,7 +489,7 @@ module ProjectsHelper end def filename_path(project, filename) - if project && blob = project.repository.send(filename) + if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend project_blob_path( project, tree_join(project.default_branch, blob.name) diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 3b175251446..456598b4c28 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -2,7 +2,7 @@ module VersionCheckHelper def version_status_badge if Rails.env.production? && current_application_settings.version_check_enabled image_url = VersionCheck.new.url - image_tag image_url, class: 'js-version-status-badge', lazy: false + image_tag image_url, class: 'js-version-status-badge' end end end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 7b617b359ea..d76c61c369f 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -11,11 +11,11 @@ module Emails @member_source_type = member_source_type @member_id = member_id - admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.members.owners_and_masters.pluck(:notification_email) # A project in a group can have no explicit owners/masters, in that case # we fallbacks to the group's owners/masters. if admins.empty? && member_source.respond_to?(:group) && member_source.group - admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.group.members.owners_and_masters.pluck(:notification_email) end mail(to: admins, diff --git a/app/models/appearance.rb b/app/models/appearance.rb index f9c48482be7..ff15689ecac 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -8,7 +8,27 @@ class Appearance < ActiveRecord::Base validates :logo, file_size: { maximum: 1.megabyte } validates :header_logo, file_size: { maximum: 1.megabyte } + validate :single_appearance_row, on: :create + mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + + CACHE_KEY = 'current_appearance'.freeze + + after_commit :flush_redis_cache + + def self.current + Rails.cache.fetch(CACHE_KEY) { first } + end + + def flush_redis_cache + Rails.cache.delete(CACHE_KEY) + end + + def single_appearance_row + if self.class.any? + errors.add(:single_appearance_row, 'Only 1 appearances row can exist') + end + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bd7c4cd45ea..8e446ff6dd8 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -241,6 +241,7 @@ class ApplicationSetting < ActiveRecord::Base performance_bar_allowed_group_id: nil, plantuml_enabled: false, plantuml_url: nil, + project_export_enabled: true, recaptcha_enabled: false, repository_checks_enabled: true, repository_storages: ['default'], diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb index 8632b8a9885..e00b47e6c17 100644 --- a/app/models/blob_viewer/notebook.rb +++ b/app/models/blob_viewer/notebook.rb @@ -2,7 +2,7 @@ module BlobViewer class Notebook < Base include Rich include ClientSide - + self.partial_name = 'notebook' self.extensions = %w(ipynb) self.binary = false diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 944725d91c3..3692bcc680d 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -14,9 +14,15 @@ class BroadcastMessage < ActiveRecord::Base default_value_for :color, '#E75E40' default_value_for :font, '#FFFFFF' + CACHE_KEY = 'broadcast_message_current'.freeze + + after_commit :flush_redis_cache + def self.current - Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do - where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a + Rails.cache.fetch(CACHE_KEY) do + where('ends_at > :now AND starts_at <= :now', now: Time.zone.now) + .reorder(id: :asc) + .to_a end end @@ -31,4 +37,8 @@ class BroadcastMessage < ActiveRecord::Base def ended? ends_at < Time.zone.now end + + def flush_redis_cache + Rails.cache.delete(CACHE_KEY) + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8be2dee6479..4692fb5644a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,10 +194,7 @@ module Ci # * Maximum length is 63 bytes # * First/Last Character is not a hyphen def ref_slug - ref.to_s - .downcase - .gsub(/[^a-z0-9]/, '-')[0..62] - .gsub(/(\A-+|-+\z)/, '') + Gitlab::Utils.slugify(ref.to_s) end # Variables whose value does not depend on environment diff --git a/app/models/commit.rb b/app/models/commit.rb index 7940733f557..d41c88b4e30 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -55,7 +55,8 @@ class Commit end def from_hash(hash, project) - new(Gitlab::Git::Commit.new(hash), project) + raw_commit = Gitlab::Git::Commit.new(project.repository.raw, hash) + new(raw_commit, project) end def valid_hash?(key) @@ -199,7 +200,7 @@ class Commit end def method_missing(m, *args, &block) - @raw.send(m, *args, &block) + @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(method, include_private = false) @@ -320,21 +321,11 @@ class Commit end def raw_diffs(*args) - if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) - Gitlab::GitalyClient::CommitService.new(project.repository).diff_from_parent(self, *args) - else - raw.diffs(*args) - end + raw.diffs(*args) end def raw_deltas - @deltas ||= Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled| - if is_enabled - Gitlab::GitalyClient::CommitService.new(project.repository).commit_deltas(self) - else - raw.deltas - end - end + @deltas ||= raw.deltas end def diffs(diff_options = nil) @@ -392,6 +383,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.new(self) + @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 48547a938fc..193e459977a 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -78,7 +78,7 @@ module CacheMarkdownField def cached_html_up_to_date?(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field) - cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? + cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend return false unless cached markdown_changed = attribute_changed?(markdown_field) || false @@ -93,14 +93,14 @@ module CacheMarkdownField end def attribute_invalidated?(attr) - __send__("#{attr}_invalidated?") + __send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend end def cached_html_for(markdown_field) raise ArgumentError.new("Unknown field: #{field}") unless cached_markdown_fields.markdown_fields.include?(markdown_field) - __send__(cached_markdown_fields.html_field(markdown_field)) + __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend end included do diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 67a0adfcd56..a3d0ac8d862 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -9,7 +9,7 @@ module InternalId def set_iid if iid.blank? parent = project || group - records = parent.send(self.class.name.tableize) + records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index c034bf9cbc0..1db6b2d2fa2 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -56,7 +56,7 @@ module Mentionable end self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) + text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend options = options.merge( cache_key: [self, attr], author: author, @@ -100,7 +100,7 @@ module Mentionable end self.class.mentionable_attrs.any? do |attr, _| - __send__(attr) =~ reference_pattern + __send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 4865c0a14b1..ce69fd34ac5 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -82,7 +82,7 @@ module Participable if attr.respond_to?(:call) source.instance_exec(current_user, ext, &attr) else - process << source.__send__(attr) + process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend end end when Enumerable, ActiveRecord::Relation diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 60734bc6660..cb59b4da3d7 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.send(:write_attribute, field, access_level) + project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 10f4be72016..78ac4f324e7 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -25,6 +25,11 @@ module Referable to_reference(from_project) end + included do + alias_method :non_referable_inspect, :inspect + alias_method :inspect, :referable_inspect + end + def referable_inspect if respond_to?(:id) "#<#{self.class.name} id:#{id} #{to_reference(full: true)}>" @@ -33,10 +38,6 @@ module Referable end end - def inspect - referable_inspect - end - module ClassMethods # The character that prefixes the actual reference identifier # diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index bd75f25a210..f2707022a4b 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -58,7 +58,7 @@ module Spammable options.fetch(:spam_title, false) end - public_send(attr.first) if attr && respond_to?(attr.first.to_sym) + public_send(attr.first) if attr && respond_to?(attr.first.to_sym) # rubocop:disable GitlabSecurity/PublicSend end def spam_description @@ -66,12 +66,12 @@ module Spammable options.fetch(:spam_description, false) end - public_send(attr.first) if attr && respond_to?(attr.first.to_sym) + public_send(attr.first) if attr && respond_to?(attr.first.to_sym) # rubocop:disable GitlabSecurity/PublicSend end def spammable_text result = self.class.spammable_attrs.map do |attr| - public_send(attr.first) + public_send(attr.first) # rubocop:disable GitlabSecurity/PublicSend end result.reject(&:blank?).join("\n") diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 1ca7f91dc03..a7d5de48c66 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -44,7 +44,8 @@ module TokenAuthenticatable end define_method("ensure_#{token_field}!") do - send("reset_#{token_field}!") if read_attribute(token_field).blank? + send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend + read_attribute(token_field) end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index ae8486bd9ac..b37b9bfbdac 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -12,7 +12,7 @@ class DeployKeysProject < ActiveRecord::Base def destroy_orphaned_deploy_key return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned? - + self.deploy_key.destroy end end diff --git a/app/models/event.rb b/app/models/event.rb index 8d93a228494..f2a560a6b56 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -48,6 +48,7 @@ class Event < ActiveRecord::Base belongs_to :author, class_name: "User" belongs_to :project belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations + has_one :push_event_payload, foreign_key: :event_id # For Hash only serialize :data # rubocop:disable Cop/ActiveRecordSerialize @@ -55,19 +56,51 @@ class Event < ActiveRecord::Base # Callbacks after_create :reset_project_activity after_create :set_last_repository_updated_at, if: :push? + after_create :replicate_event_for_push_events_migration # Scopes scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } - scope :in_projects, ->(projects) do - where(project_id: projects.pluck(:id)).recent + scope :in_projects, -> (projects) do + sub_query = projects + .except(:order) + .select(1) + .where('projects.id = events.project_id') + + where('EXISTS (?)', sub_query).recent + end + + scope :with_associations, -> do + # We're using preload for "push_event_payload" as otherwise the association + # is not always available (depending on the query being built). + includes(:author, :project, project: :namespace) + .preload(:target, :push_event_payload) end - scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } + self.inheritance_column = 'action' + class << self + def find_sti_class(action) + if action.to_i == PUSHED + PushEvent + else + Event + end + end + + def subclass_from_attributes(attrs) + # Without this Rails will keep calling this method on the returned class, + # resulting in an infinite loop. + return unless self == Event + + action = attrs.with_indifferent_access[inheritance_column].to_i + + PushEvent if action == PUSHED + end + # Update Gitlab::ContributionsCalendar#activity_dates if this changes def contributions where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)", @@ -290,6 +323,16 @@ class Event < ActiveRecord::Base @commits ||= (data[:commits] || []).reverse end + def commit_title + commit = commits.last + + commit[:message] if commit + end + + def commit_id + commit_to || commit_from + end + def commits_count data[:total_commits_count] || commits.count || 0 end @@ -385,6 +428,16 @@ class Event < ActiveRecord::Base user ? author_id == user.id : false end + # We're manually replicating data into the new table since database triggers + # are not dumped to db/schema.rb. This could mean that a new installation + # would not have the triggers in place, thus losing events data in GitLab + # 10.0. + def replicate_event_for_push_events_migration + new_attributes = attributes.with_indifferent_access.except(:title, :data) + + EventForMigration.create!(new_attributes) + end + private def recent_update? diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb new file mode 100644 index 00000000000..8b8244314af --- /dev/null +++ b/app/models/event_collection.rb @@ -0,0 +1,98 @@ +# A collection of events to display in an event list. +# +# An EventCollection is meant to be used for displaying events to a user (e.g. +# in a controller), it's not suitable for building queries that are used for +# building other queries. +class EventCollection + # To prevent users from putting too much pressure on the database by cycling + # through thousands of events we put a limit on the number of pages. + MAX_PAGE = 10 + + # projects - An ActiveRecord::Relation object that returns the projects for + # which to retrieve events. + # filter - An EventFilter instance to use for filtering events. + def initialize(projects, limit: 20, offset: 0, filter: nil) + @projects = projects + @limit = limit + @offset = offset + @filter = filter + end + + # Returns an Array containing the events. + def to_a + return [] if current_page > MAX_PAGE + + relation = if Gitlab::Database.join_lateral_supported? + relation_with_join_lateral + else + relation_without_join_lateral + end + + relation.with_associations.to_a + end + + private + + # Returns the events relation to use when JOIN LATERAL is not supported. + # + # This relation simply gets all the events for all authorized projects, then + # limits that set. + def relation_without_join_lateral + events = filtered_events.in_projects(projects) + + paginate_events(events) + end + + # Returns the events relation to use when JOIN LATERAL is supported. + # + # This relation is built using JOIN LATERAL, producing faster queries than a + # regular LIMIT + OFFSET approach. + def relation_with_join_lateral + projects_for_lateral = projects.select(:id).to_sql + + lateral = filtered_events + .limit(limit_for_join_lateral) + .where('events.project_id = projects_for_lateral.id') + .to_sql + + # The outer query does not need to re-apply the filters since the JOIN + # LATERAL body already takes care of this. + outer = base_relation + .from("(#{projects_for_lateral}) projects_for_lateral") + .joins("JOIN LATERAL (#{lateral}) AS #{Event.table_name} ON true") + + paginate_events(outer) + end + + def filtered_events + @filter ? @filter.apply_filter(base_relation) : base_relation + end + + def paginate_events(events) + events.limit(@limit).offset(@offset) + end + + def base_relation + # We want to have absolute control over the event queries being built, thus + # we're explicitly opting out of any default scopes that may be set. + Event.unscoped.recent + end + + def limit_for_join_lateral + # Applying the OFFSET on the inside of a JOIN LATERAL leads to incorrect + # results. To work around this we need to increase the inner limit for every + # page. + # + # This means that on page 1 we use LIMIT 20, and an outer OFFSET of 0. On + # page 2 we use LIMIT 40 and an outer OFFSET of 20. + @limit + @offset + end + + def current_page + (@offset / @limit) + 1 + end + + def projects + @projects.except(:order) + end +end diff --git a/app/models/event_for_migration.rb b/app/models/event_for_migration.rb new file mode 100644 index 00000000000..a1672da5eec --- /dev/null +++ b/app/models/event_for_migration.rb @@ -0,0 +1,5 @@ +# This model is used to replicate events between the old "events" table and the +# new "events_for_migration" table that will replace "events" in GitLab 10.0. +class EventForMigration < ActiveRecord::Base + self.table_name = 'events_for_migration' +end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 1ac0e123ff1..50fb35c77ec 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -18,4 +18,8 @@ class GpgSignature < ActiveRecord::Base def commit project.commit(commit_sha) end + + def gpg_commit + Gitlab::Gpg::Commit.new(project, commit_sha) + end end diff --git a/app/models/group.rb b/app/models/group.rb index bd5735ed82e..2816a68257c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -212,21 +212,39 @@ class Group < Namespace end def user_ids_for_project_authorizations - users_with_parents.pluck(:id) + members_with_parents.pluck(:user_id) end def members_with_parents - GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) + # Avoids an unnecessary SELECT when the group has no parents + source_ids = + if parent_id + self_and_ancestors.reorder(nil).select(:id) + else + id + end + + GroupMember + .active_without_invites + .where(source_id: source_ids) + end + + def members_with_descendants + GroupMember + .active_without_invites + .where(source_id: self_and_descendants.reorder(nil).select(:id)) end def users_with_parents - User.where(id: members_with_parents.select(:user_id)) + User + .where(id: members_with_parents.select(:user_id)) + .reorder(nil) end def users_with_descendants - members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) - - User.where(id: members_with_descendants.select(:user_id)) + User + .where(id: members_with_descendants.select(:user_id)) + .reorder(nil) end def max_member_access_for_user(user) diff --git a/app/models/member.rb b/app/models/member.rb index dc9247bc9a0..ee2cb13697b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -41,9 +41,20 @@ class Member < ActiveRecord::Base is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) user_is_active = User.arel_table[:state].eq(:active) - includes(:user).references(:users) - .where(is_external_invite.or(user_is_active)) + user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active) + + left_join_users + .where(user_ok) .where(requested_at: nil) + .reorder(nil) + end + + # Like active, but without invites. For when a User is required. + scope :active_without_invites, -> do + left_join_users + .where(users: { state: 'active' }) + .where(requested_at: nil) + .reorder(nil) end scope :invite, -> { where.not(invite_token: nil) } @@ -276,6 +287,13 @@ class Member < ActiveRecord::Base @notification_setting ||= user.notification_settings_for(source) end + def notifiable?(type, opts = {}) + # always notify when there isn't a user yet + return true if user.blank? + + NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts)) + end + private def send_invite @@ -332,4 +350,8 @@ class Member < ActiveRecord::Base def notification_service NotificationService.new end + + def notifiable_options + {} + end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 47040f95533..661e668dbf9 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -30,6 +30,10 @@ class GroupMember < Member 'Group' end + def notifiable_options + { group: group } + end + private def send_invite diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index c0e17f4bfc8..b6f1dd272cd 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -87,6 +87,10 @@ class ProjectMember < Member project.owner == user end + def notifiable_options + { project: project } + end + private def delete_member_todos diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index e83b11f7668..ac08dc0ee1f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -162,7 +162,7 @@ class MergeRequest < ActiveRecord::Base target = unscoped.where(target_project_id: relation).select(:id) union = Gitlab::SQL::Union.new([source, target]) - where("merge_requests.id IN (#{union.to_sql})") + where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze @@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base end def reload_diff_if_branch_changed - if source_branch_changed? || target_branch_changed? + if (source_branch_changed? || target_branch_changed?) && + (source_branch_head && target_branch_head) reload_diff end end @@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base end def fetch_ref - target_project.repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{source_branch}", - ref_path - ) + write_ref update_column(:ref_fetched, true) end @@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base true end + + private + + def write_ref + target_project.repository.with_repo_branch_commit( + source_project.repository, source_branch) do |commit| + if commit + target_project.repository.write_ref(ref_path, commit.sha) + else + raise Rugged::ReferenceError, 'source repository is empty' + end + end + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index d9d746ccf41..58050e1f438 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -282,9 +282,7 @@ class MergeRequestDiff < ActiveRecord::Base def load_commits commits = st_commits.presence || merge_request_diff_commits - commits.map do |commit| - Commit.new(Gitlab::Git::Commit.new(commit.to_hash), merge_request.source_project) - end + commits.map { |commit| Commit.from_hash(commit.to_hash, project) } end def save_diffs diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index cafdbe11849..670b26d4ca3 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -26,7 +26,7 @@ class MergeRequestDiffCommit < ActiveRecord::Base def to_hash Gitlab::Git::Commit::SERIALIZE_KEYS.each_with_object({}) do |key, hash| - hash[key] = public_send(key) + hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6073fb94a3f..e7bc1d1b080 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base .base_and_ancestors end + def self_and_ancestors + return self.class.where(id: id) unless parent_id + + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_ancestors + end + # Returns all the descendants of the current namespace. def descendants Gitlab::GroupHierarchy @@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base .base_and_descendants end + def self_and_descendants + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_descendants + end + def user_ids_for_project_authorizations [owner_id] end diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 8417f200e36..9357e55b419 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -12,7 +12,7 @@ module Network end def method_missing(m, *args, &block) - @commit.send(m, *args, &block) + @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def space diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 2bc00a082df..0e5acb22d50 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -206,7 +206,7 @@ module Network # Visit branching chains leaves.each do |l| - parents = l.parents(@map).select{|p| p.space.zero?} + parents = l.parents(@map).select {|p| p.space.zero?} parents.each do |p| place_chain(p, l.time) end diff --git a/app/models/note.rb b/app/models/note.rb index d0e3bc0bfed..a752c897d63 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -77,20 +77,20 @@ class Note < ActiveRecord::Base # Scopes scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } - scope :system, ->{ where(system: true) } - scope :user, ->{ where(system: false) } - scope :common, ->{ where(noteable_type: ["", nil]) } - scope :fresh, ->{ order(created_at: :asc, id: :asc) } - scope :updated_after, ->(time){ where('updated_at > ?', time) } - scope :inc_author_project, ->{ includes(:project, :author) } - scope :inc_author, ->{ includes(:author) } + scope :system, -> { where(system: true) } + scope :user, -> { where(system: false) } + scope :common, -> { where(noteable_type: ["", nil]) } + scope :fresh, -> { order(created_at: :asc, id: :asc) } + scope :updated_after, ->(time) { where('updated_at > ?', time) } + scope :inc_author_project, -> { includes(:project, :author) } + scope :inc_author, -> { includes(:author) } scope :inc_relations_for_view, -> do includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata) end - scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) } - scope :new_diff_notes, ->{ where(type: 'DiffNote') } - scope :non_diff_notes, ->{ where(type: ['Note', 'DiscussionNote', nil]) } + scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) } + scope :new_diff_notes, -> { where(type: 'DiffNote') } + scope :non_diff_notes, -> { where(type: ['Note', 'DiscussionNote', nil]) } scope :with_associations, -> do # FYI noteable cannot be loaded for LegacyDiffNote for commits diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 418b42d8f1d..dc862565a71 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -5,14 +5,22 @@ class NotificationRecipient custom_action: nil, target: nil, acting_user: nil, - project: nil + project: nil, + group: nil, + skip_read_ability: false ) + unless NotificationSetting.levels.key?(type) || type == :subscription + raise ArgumentError, "invalid type: #{type.inspect}" + end + @custom_action = custom_action @acting_user = acting_user @target = target - @project = project || @target&.project + @project = project || default_project + @group = group || @project&.group @user = user @type = type + @skip_read_ability = skip_read_ability end def notification_setting @@ -77,6 +85,8 @@ class NotificationRecipient def has_access? DeclarativePolicy.subject_scope do return false unless user.can?(:receive_notifications) + return true if @skip_read_ability + return false if @project && !user.can?(:read_project, @project) return true unless read_ability @@ -96,6 +106,7 @@ class NotificationRecipient private def read_ability + return nil if @skip_read_ability return @read_ability if instance_variable_defined?(:@read_ability) @read_ability = @@ -111,12 +122,18 @@ class NotificationRecipient end end + def default_project + return nil if @target.nil? + return @target if @target.is_a?(Project) + return @target.project if @target.respond_to?(:project) + end + def find_notification_setting project_setting = @project && user.notification_settings_for(@project) return project_setting unless project_setting.nil? || project_setting.global? - group_setting = @project&.group && user.notification_settings_for(@project.group) + group_setting = @group && user.notification_settings_for(@group) return group_setting unless group_setting.nil? || group_setting.global? diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 9b1cac64c44..245f8dddcf9 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -66,6 +66,6 @@ class NotificationSetting < ActiveRecord::Base alias_method :failed_pipeline?, :failed_pipeline def event_enabled?(event) - respond_to?(event) && !!public_send(event) + respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/project.rb b/app/models/project.rb index e7baba2ef08..be248bc99e1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -60,7 +60,7 @@ class Project < ActiveRecord::Base end before_destroy :remove_private_deploy_keys - after_destroy :remove_pages + after_destroy -> { run_after_commit { remove_pages } } # update visibility_level of forks after_update :update_forks_visibility_level @@ -196,7 +196,6 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :import_data delegate :name, to: :owner, allow_nil: true, prefix: true - delegate :count, to: :forks, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team @@ -370,7 +369,10 @@ class Project < ActiveRecord::Base state :failed after_transition [:none, :finished, :failed] => :scheduled do |project, _| - project.run_after_commit { add_import_job } + project.run_after_commit do + job_id = add_import_job + update(import_jid: job_id) if job_id + end end after_transition started: :finished do |project, _| @@ -415,7 +417,7 @@ class Project < ActiveRecord::Base union = Gitlab::SQL::Union.new([projects, namespaces]) - where("projects.id IN (#{union.to_sql})") + where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end def search_by_title(query) @@ -525,17 +527,26 @@ class Project < ActiveRecord::Base def add_import_job job_id = if forked? - RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, - forked_from_project.full_path, - self.namespace.full_path) + RepositoryForkWorker.perform_async(id, + forked_from_project.repository_storage_path, + forked_from_project.full_path, + self.namespace.full_path) else RepositoryImportWorker.perform_async(self.id) end + log_import_activity(job_id) + + job_id + end + + def log_import_activity(job_id, type: :import) + job_type = type.to_s.capitalize + if job_id - Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}" + Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.") else - Rails.logger.error "Import job failed to start for #{full_path}" + Rails.logger.error("#{job_type} job failed to create for #{full_path}.") end end @@ -544,6 +555,7 @@ class Project < ActiveRecord::Base ProjectCacheWorker.perform_async(self.id) end + update(import_error: nil) remove_import_data end @@ -825,7 +837,7 @@ class Project < ActiveRecord::Base if template.nil? # If no template, we should create an instance. Ex `build_gitlab_ci_service` - public_send("build_#{service_name}_service") + public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend else Service.build_from_template(id, template) end @@ -921,14 +933,14 @@ class Project < ActiveRecord::Base end def execute_hooks(data, hooks_scope = :push_hooks) - hooks.send(hooks_scope).each do |hook| + hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, hooks_scope.to_s) end end def execute_services(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope - services.send(hooks_scope).each do |service| + services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend service.async_execute(data) end end @@ -1046,13 +1058,16 @@ class Project < ActiveRecord::Base end def change_head(branch) - repository.before_change_head - repository.rugged.references.create('HEAD', - "refs/heads/#{branch}", - force: true) - repository.copy_gitattributes(branch) - repository.after_change_head - reload_default_branch + if repository.branch_exists?(branch) + repository.before_change_head + repository.write_ref('HEAD', "refs/heads/#{branch}") + repository.copy_gitattributes(branch) + repository.after_change_head + reload_default_branch + else + errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist") + false + end end def forked_from?(project) @@ -1222,6 +1237,9 @@ class Project < ActiveRecord::Base # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal? def remove_pages + # Projects with a missing namespace cannot have their pages removed + return unless namespace + ::Projects::UpdatePagesConfigurationService.new(self).execute # 1. We rename pages to temporary directory @@ -1280,12 +1298,16 @@ class Project < ActiveRecord::Base status.zero? end + def full_path_slug + Gitlab::Utils.slugify(full_path.to_s) + end + def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: path, public: true }, { key: 'CI_PROJECT_PATH', value: full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: full_path.parameterize, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: web_url, public: true } ] @@ -1326,7 +1348,7 @@ class Project < ActiveRecord::Base end def append_or_update_attribute(name, value) - old_values = public_send(name.to_s) + old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any? update_attribute(name, old_values + value) @@ -1393,6 +1415,10 @@ class Project < ActiveRecord::Base # @deprecated cannot remove yet because it has an index with its name in elasticsearch alias_method :path_with_namespace, :full_path + def forks_count + Projects::ForksCountService.new(self).count + end + private def cross_namespace_reference?(from) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index c8fabb16dc1..fb1db0255aa 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -55,7 +55,7 @@ class ProjectFeature < ActiveRecord::Base end def access_level(feature) - public_send(ProjectFeature.access_level_attribute(feature)) + public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend end def builds_enabled? @@ -80,7 +80,7 @@ class ProjectFeature < ActiveRecord::Base # which cannot be higher than repository access level def repository_children_level validator = lambda do |field| - level = public_send(field) || ProjectFeature::ENABLED + level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend not_allowed = level > repository_access_level self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 6d1a321f651..7b15a5dd04d 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -115,7 +115,7 @@ class ChatNotificationService < Service def get_channel_field(event) field_name = event_channel_name(event) - self.public_send(field_name) + self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend end def build_event_channels diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index e3906943ecd..f422e0ea036 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -53,7 +53,7 @@ class HipchatService < Service return unless supported_events.include?(data[:object_kind]) message = create_message(data) return unless message.present? - gate[room].send('GitLab', message, message_options(data)) + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend end def test(data) diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index aeaf63abab9..715b215d1db 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -14,7 +14,7 @@ class ProjectStatistics < ActiveRecord::Base def refresh!(only: nil) STATISTICS_COLUMNS.each do |column, generator| if only.blank? || only.include?(column) - public_send("update_#{column}") + public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index 122fbce257d..c96edc5a259 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -1,5 +1,9 @@ class ProtectableDropdown + REF_TYPES = %i[branches tags].freeze + def initialize(project, ref_type) + raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES) + @project = project @ref_type = ref_type end @@ -16,7 +20,7 @@ class ProtectableDropdown private def refs - @project.repository.public_send(@ref_type) + @project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend end def ref_names @@ -24,7 +28,7 @@ class ProtectableDropdown end def protections - @project.public_send("protected_#{@ref_type}") + @project.public_send("protected_#{@ref_type}") # rubocop:disable GitlabSecurity/PublicSend end def non_wildcard_protected_ref_names diff --git a/app/models/push_event.rb b/app/models/push_event.rb new file mode 100644 index 00000000000..3f1ff979de6 --- /dev/null +++ b/app/models/push_event.rb @@ -0,0 +1,126 @@ +class PushEvent < Event + # This validation exists so we can't accidentally use PushEvent with a + # different "action" value. + validate :validate_push_action + + # Authors are required as they're used to display who pushed data. + # + # We're just validating the presence of the ID here as foreign key constraints + # should ensure the ID points to a valid user. + validates :author_id, presence: true + + # The project is required to build links to commits, commit ranges, etc. + # + # We're just validating the presence of the ID here as foreign key constraints + # should ensure the ID points to a valid project. + validates :project_id, presence: true + + # The "data" field must not be set for push events since it's not used and a + # waste of space. + validates :data, absence: true + + # These fields are also not used for push events, thus storing them would be a + # waste. + validates :target_id, absence: true + validates :target_type, absence: true + + def self.sti_name + PUSHED + end + + def push? + true + end + + def push_with_commits? + !!(commit_from && commit_to) + end + + def tag? + return super unless push_event_payload + + push_event_payload.tag? + end + + def branch? + return super unless push_event_payload + + push_event_payload.branch? + end + + def valid_push? + return super unless push_event_payload + + push_event_payload.ref.present? + end + + def new_ref? + return super unless push_event_payload + + push_event_payload.created? + end + + def rm_ref? + return super unless push_event_payload + + push_event_payload.removed? + end + + def commit_from + return super unless push_event_payload + + push_event_payload.commit_from + end + + def commit_to + return super unless push_event_payload + + push_event_payload.commit_to + end + + def ref_name + return super unless push_event_payload + + push_event_payload.ref + end + + def ref_type + return super unless push_event_payload + + push_event_payload.ref_type + end + + def branch_name + return super unless push_event_payload + + ref_name + end + + def tag_name + return super unless push_event_payload + + ref_name + end + + def commit_title + return super unless push_event_payload + + push_event_payload.commit_title + end + + def commit_id + commit_to || commit_from + end + + def commits_count + return super unless push_event_payload + + push_event_payload.commit_count + end + + def validate_push_action + return if action == PUSHED + + errors.add(:action, "the action #{action.inspect} is not valid") + end +end diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb new file mode 100644 index 00000000000..6cdb1cd4fe9 --- /dev/null +++ b/app/models/push_event_payload.rb @@ -0,0 +1,22 @@ +class PushEventPayload < ActiveRecord::Base + include ShaAttribute + + belongs_to :event, inverse_of: :push_event_payload + + validates :event_id, :commit_count, :action, :ref_type, presence: true + validates :commit_title, length: { maximum: 70 } + + sha_attribute :commit_from + sha_attribute :commit_to + + enum action: { + created: 0, + removed: 1, + pushed: 2 + } + + enum ref_type: { + branch: 0, + tag: 1 + } +end diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 964175ddab8..31de204d824 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -8,5 +8,13 @@ class RedirectRoute < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - scope :matching_path_and_descendants, -> (path) { where('redirect_routes.path = ? OR redirect_routes.path LIKE ?', path, "#{sanitize_sql_like(path)}/%") } + scope :matching_path_and_descendants, -> (path) do + wheres = if Gitlab::Database.postgresql? + 'LOWER(redirect_routes.path) = LOWER(?) OR LOWER(redirect_routes.path) LIKE LOWER(?)' + else + 'redirect_routes.path = ? OR redirect_routes.path LIKE ?' + end + + where(wheres, path, "#{sanitize_sql_like(path)}/%") + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 3b5d0e00c70..c1e4fcf94a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -48,7 +48,9 @@ class Repository alias_method(original, name) define_method(name) do - cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) } + cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do + __send__(original) # rubocop:disable GitlabSecurity/PublicSend + end end end @@ -64,6 +66,8 @@ class Repository @raw_repository ||= initialize_raw_repository end + alias_method :raw, :raw_repository + # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( @@ -222,7 +226,7 @@ class Repository # This will still fail if the file is corrupted (e.g. 0 bytes) begin - rugged.references.create(keep_around_ref_name(sha), sha, force: true) + write_ref(keep_around_ref_name(sha), sha) rescue Rugged::ReferenceError => ex Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex @@ -235,6 +239,10 @@ class Repository ref_exists?(keep_around_ref_name(sha)) end + def write_ref(ref_path, sha) + rugged.references.create(ref_path, sha, force: true) + end + def diverging_commit_counts(branch) root_ref_hash = raw_repository.rev_parse_target(root_ref).oid cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -298,7 +306,7 @@ class Repository expire_method_caches(to_refresh) - to_refresh.each { |method| send(method) } + to_refresh.each { |method| send(method) } # rubocop:disable GitlabSecurity/PublicSend end def expire_branch_cache(branch_name = nil) @@ -437,9 +445,9 @@ class Repository def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} - lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block) + lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend else - raw_repository.send(m, *args, &block) + raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end end @@ -763,14 +771,14 @@ class Repository index = Gitlab::Git::Index.new(raw_repository) if start_commit - index.read_tree(start_commit.raw_commit.tree) + index.read_tree(start_commit.rugged_commit.tree) parents = [start_commit.sha] else parents = [] end actions.each do |options| - index.public_send(options.delete(:action), options) + index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend end options = { @@ -983,12 +991,10 @@ class Repository if start_repository == self start_branch_name else - tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" - - fetch_ref( + tmp_ref = fetch_ref( start_repository.path_to_repo, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - tmp_ref + "refs/tmp/#{SecureRandom.hex}/head" ) start_repository.commit(start_branch_name).sha @@ -1019,7 +1025,12 @@ class Repository def fetch_ref(source_path, source_ref, target_ref) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - run_git(args) + message, status = run_git(args) + + # Make sure ref was created, and raise Rugged::ReferenceError when not + raise Rugged::ReferenceError, message if status != 0 + + target_ref end def create_ref(ref, ref_path) diff --git a/app/models/user.rb b/app/models/user.rb index 5148886eed7..02c3ab6654b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,11 +47,6 @@ class User < ActiveRecord::Base devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable - # devise overrides #inspect, so we manually use the Referable one - def inspect - referable_inspect - end - # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour def update_tracked_fields!(request) @@ -148,6 +143,8 @@ class User < ActiveRecord::Base uniqueness: { case_sensitive: false } validate :namespace_uniq, if: :username_changed? + validate :namespace_move_dir_allowed, if: :username_changed? + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? @@ -487,6 +484,12 @@ class User < ActiveRecord::Base end end + def namespace_move_dir_allowed + if namespace&.any_project_has_container_registry_tags? + errors.add(:username, 'cannot be changed if a personal project has container registry tags.') + end + end + def avatar_type unless avatar.image? errors.add :avatar, "only images allowed" @@ -528,7 +531,7 @@ class User < ActiveRecord::Base union = Gitlab::SQL::Union .new([groups.select(:id), authorized_projects.select(:namespace_id)]) - Group.where("namespaces.id IN (#{union.to_sql})") + Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end # Returns a relation of groups the user has access to, including their parent @@ -718,9 +721,9 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w[username skype linkedin twitter].each do |attr| - value = public_send(attr) - public_send("#{attr}=", Sanitize.clean(value)) if value.present? + %i[skype linkedin twitter].each do |attr| + value = self[attr] + self[attr] = Sanitize.clean(value) if value.present? end end @@ -779,7 +782,7 @@ class User < ActiveRecord::Base def with_defaults User.defaults.each do |k, v| - public_send("#{k}=", v) + public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend end self @@ -825,7 +828,7 @@ class User < ActiveRecord::Base { name: name, username: username, - avatar_url: avatar_url + avatar_url: avatar_url(only_path: false) } end @@ -919,7 +922,7 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin runner_ids = Ci::RunnerProject - .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") + .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection .select(:runner_id) Ci::Runner.specific.where(id: runner_ids) end @@ -1061,7 +1064,8 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) - devise_mailer.send(notification, self, *args).deliver_later + return true unless can?(:receive_notifications) + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend end # This works around a bug in Devise 4.2.0 that erroneously causes a user to diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb index ad7ad020b03..bdc22d71202 100644 --- a/app/serializers/analytics_build_entity.rb +++ b/app/serializers/analytics_build_entity.rb @@ -35,6 +35,6 @@ class AnalyticsBuildEntity < Grape::Entity private def url_to(route, build, id = nil) - public_send("#{route}_url", build.project.namespace, build.project, id || build) + public_send("#{route}_url", build.project.namespace, build.project, id || build) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb index 44c50f18613..b7d95ea020f 100644 --- a/app/serializers/analytics_issue_entity.rb +++ b/app/serializers/analytics_issue_entity.rb @@ -24,6 +24,6 @@ class AnalyticsIssueEntity < Grape::Entity private def url_to(route, id) - public_send("#{route}_url", request.project.namespace, request.project, id) + public_send("#{route}_url", request.project.namespace, request.project, id) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index d6de43bcbcb..72e56a2c77f 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -46,6 +46,6 @@ class JobEntity < Grape::Entity end def path_to(route, build) - send("#{route}_path", build.project.namespace, build.project, build) + send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb index dc283ba3e7a..b3e5fd21e97 100644 --- a/app/serializers/project_entity.rb +++ b/app/serializers/project_entity.rb @@ -1,6 +1,6 @@ class ProjectEntity < Grape::Entity include RequestAwareEntity - + expose :id expose :name diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb index 23b65aa4a4c..69702ae1493 100644 --- a/app/serializers/tree_root_entity.rb +++ b/app/serializers/tree_root_entity.rb @@ -1,8 +1,21 @@ # TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`. class TreeRootEntity < Grape::Entity + include RequestAwareEntity + expose :path - + expose :trees, using: TreeEntity expose :blobs, using: BlobEntity expose :submodules, using: SubmoduleEntity + + expose :parent_tree_url do |tree| + path = tree.path.sub(%r{\A/}, '') + next unless path.present? + + path_segments = path.split('/') + path_segments.pop + parent_tree_path = path_segments.join('/') + + project_tree_path(request.project, File.join(request.ref, parent_tree_path)) + end end diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb index 8e11a2a36a7..59153cbbc0a 100644 --- a/app/services/akismet_service.rb +++ b/app/services/akismet_service.rb @@ -58,7 +58,7 @@ class AkismetService } begin - akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) + akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend true rescue => e Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index fc87bd6a659..414f672cc6a 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -85,13 +85,13 @@ module Ci end def register_failure - failed_attempt_counter.increase - attempt_counter.increase + failed_attempt_counter.increment + attempt_counter.increment end def register_success(job) job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) - attempt_counter.increase + attempt_counter.increment end def failed_attempt_counter diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6372e5755db..ea3b8d66ed9 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -23,7 +23,7 @@ module Ci end attributes = CLONE_ACCESSORS.map do |attribute| - [attribute, build.send(attribute)] + [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend end attributes.push([:user, current_user]) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index a48d6a976f0..85c2fcf9ea6 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -11,6 +11,7 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) + # rubocop:disable GitlabSecurity/PublicSend repository.public_send( action, current_user, diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 0f3a485a3fd..0b7e4f187f7 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -71,7 +71,14 @@ class EventCreateService end def push(project, current_user, push_data) - create_event(project, current_user, Event::PUSHED, data: push_data) + # We're using an explicit transaction here so that any errors that may occur + # when creating push payload data will result in the event creation being + # rolled back as well. + Event.transaction do + event = create_event(project, current_user, Event::PUSHED) + + PushEventPayloadService.new(event, push_data).execute + end Users::ActivityService.new(current_user, 'push').execute end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index ada2b64a3a6..e81a56672e2 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -90,8 +90,19 @@ class GitPushService < BaseService end def update_signatures - @push_commits.each do |commit| - CreateGpgSignatureWorker.perform_async(commit.sha, @project.id) + commit_shas = @push_commits.last(PROCESS_COMMIT_LIMIT).map(&:sha) + + return if commit_shas.empty? + + shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) + commit_shas -= shas_with_cached_signatures + + return if commit_shas.empty? + + commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas) + + commit_shas.each do |sha| + CreateGpgSignatureWorker.perform_async(sha, project.id) end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b84a6fd2b7d..4a4f2b91182 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -338,7 +338,7 @@ class IssuableBaseService < BaseService def invalidate_cache_counts(issuable, users: [], skip_project_cache: false) users.each do |user| - user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") + user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend end unless skip_project_cache diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 9114f0ccc81..234fcbede03 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -16,6 +16,7 @@ module Issues spam_check(issue, current_user) issue.move_to_end + # current_user (defined in BaseService) is not available within run_after_commit block user = current_user issue.run_after_commit do NewIssueWorker.perform_async(issue.id, user.id) diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index d2ece354efc..775efed48eb 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -37,7 +37,7 @@ module Labels union = Gitlab::SQL::Union.new(label_ids) - Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq + Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq # rubocop:disable GitlabSecurity/SqlInjection end def group_labels_applied_to_issues diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 2e089149ca8..46c505baf8b 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -31,7 +31,7 @@ module Members source.members.find_by(condition) || source.requesters.find_by!(condition) else - source.public_send(scope).find_by!(condition) + source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 7d539fa49e6..194413bf321 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -17,6 +17,7 @@ module MergeRequests end def before_create(merge_request) + # current_user (defined in BaseService) is not available within run_after_commit block user = current_user merge_request.run_after_commit do NewMergeRequestWorker.perform_async(merge_request.id, user.id) @@ -24,7 +25,6 @@ module MergeRequests end def after_create(issuable) - event_service.open_mr(issuable, current_user) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) update_merge_requests_head_pipeline(issuable) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index df04b1a4fe3..e2a80db06a6 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -1,3 +1,5 @@ +# rubocop:disable GitlabSecurity/PublicSend + # NotificationService class # # Used for notifying users with emails about different events @@ -10,9 +12,11 @@ class NotificationService # only if ssh key is not deploy key # # This is security email so it will be sent - # even if user disabled notifications + # even if user disabled notifications. However, + # it won't be sent to internal users like the + # ghost user or the EE support bot. def new_key(key) - if key.user + if key.user&.can?(:receive_notifications) mailer.new_ssh_key_email(key.id).deliver_later end end @@ -22,14 +26,14 @@ class NotificationService # This is a security email so it will be sent even if the user user disabled # notifications def new_gpg_key(gpg_key) - if gpg_key.user + if gpg_key.user&.can?(:receive_notifications) mailer.new_gpg_key_email(gpg_key.id).deliver_later end end # Always notify user about email added to profile def new_email(email) - if email.user + if email.user&.can?(:receive_notifications) mailer.new_email_email(email.id).deliver_later end end @@ -185,6 +189,8 @@ class NotificationService # Notify new user with email after creation def new_user(user, token = nil) + return true unless notifiable?(user, :mention) + # Don't email omniauth created users mailer.new_user_email(user.id, token).deliver_later unless user.identities.any? end @@ -206,19 +212,27 @@ class NotificationService # Members def new_access_request(member) + return true unless member.notifiable?(:subscription) + mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later end def decline_access_request(member) + return true unless member.notifiable?(:subscription) + mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later end # Project invite def invite_project_member(project_member, token) + return true unless project_member.notifiable?(:subscription) + mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later end def accept_project_invite(project_member) + return true unless project_member.notifiable?(:subscription) + mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later end @@ -232,10 +246,14 @@ class NotificationService end def new_project_member(project_member) + return true unless project_member.notifiable?(:mention, skip_read_ability: true) + mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end def update_project_member(project_member) + return true unless project_member.notifiable?(:mention) + mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end @@ -249,6 +267,9 @@ class NotificationService end def decline_group_invite(group_member) + # always send this one, since it's a response to the user's own + # action + mailer.member_invite_declined_email( group_member.real_source_type, group_member.group.id, @@ -258,15 +279,19 @@ class NotificationService end def new_group_member(group_member) + return true unless group_member.notifiable?(:mention) + mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end def update_group_member(group_member) + return true unless group_member.notifiable?(:mention) + mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end def project_was_moved(project, old_path_with_namespace) - recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project) + recipients = notifiable_users(project.team.members, :mention, project: project) recipients.each do |recipient| mailer.project_was_moved_email( @@ -288,10 +313,14 @@ class NotificationService end def project_exported(project, current_user) + return true unless notifiable?(current_user, :mention, project: project) + mailer.project_was_exported_email(current_user, project).deliver_later end def project_not_exported(project, current_user, errors) + return true unless notifiable?(current_user, :mention, project: project) + mailer.project_was_not_exported_email(current_user, project, errors).deliver_later end @@ -300,7 +329,7 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= NotificationRecipientService.notifiable_users( + recipients ||= notifiable_users( [pipeline.user], :watch, custom_action: :"#{pipeline.status}_pipeline", target: pipeline @@ -369,7 +398,7 @@ class NotificationService def relabeled_resource_email(target, labels, current_user, method) recipients = labels.flat_map { |l| l.subscribers(target.project) } - recipients = NotificationRecipientService.notifiable_users( + recipients = notifiable_users( recipients, :subscription, target: target, acting_user: current_user @@ -401,4 +430,14 @@ class NotificationService object.previous_changes[attribute].first end end + + private + + def notifiable?(*args) + NotificationRecipientService.notifiable?(*args) + end + + def notifiable_users(*args) + NotificationRecipientService.notifiable_users(*args) + end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 11ad4838471..54eb75ab9bf 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -128,6 +128,8 @@ module Projects project.repository.before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete + + Projects::ForksCountService.new(project).delete_cache end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a2b23ea6171..ad67e68a86a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -21,11 +21,17 @@ module Projects builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + refresh_forks_count + new_project end private + def refresh_forks_count + Projects::ForksCountService.new(@project).refresh_cache + end + def allowed_visibility_level project_level = @project.visibility_level diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb new file mode 100644 index 00000000000..e2e2b1da91d --- /dev/null +++ b/app/services/projects/forks_count_service.rb @@ -0,0 +1,30 @@ +module Projects + # Service class for getting and caching the number of forks of a project. + class ForksCountService + def initialize(project) + @project = project + end + + def count + Rails.cache.fetch(cache_key) { uncached_count } + end + + def refresh_cache + Rails.cache.write(cache_key, uncached_count) + end + + def delete_cache + Rails.cache.delete(cache_key) + end + + private + + def uncached_count + @project.forks.count + end + + def cache_key + ['projects', @project.id, 'forks_count'] + end + end +end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f385e426827..f30b40423c8 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -13,7 +13,13 @@ module Projects ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end + refresh_forks_count(@project.forked_from_project) + @project.forked_project_link.destroy end + + def refresh_forks_count(project) + Projects::ForksCountService.new(project).refresh_cache + end end end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 5038155ca31..394b336a638 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -172,11 +172,11 @@ module Projects end def register_attempt - pages_deployments_total_counter.increase + pages_deployments_total_counter.increment end def register_failure - pages_deployments_failed_total_counter.increase + pages_deployments_failed_total_counter.increment end def pages_deployments_total_counter diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index d81035e4eba..cf69007bc3b 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -10,7 +10,7 @@ module Projects end if changing_default_branch? - project.change_head(params[:default_branch]) + return error("Could not set the default branch") unless project.change_head(params[:default_branch]) end if project.update_attributes(params.except(:default_branch)) diff --git a/app/services/push_event_payload_service.rb b/app/services/push_event_payload_service.rb new file mode 100644 index 00000000000..b0a389c85f9 --- /dev/null +++ b/app/services/push_event_payload_service.rb @@ -0,0 +1,120 @@ +# Service class for creating push event payloads as stored in the +# "push_event_payloads" table. +# +# Example: +# +# data = Gitlab::DataBuilder::Push.build(...) +# event = Event.create(...) +# +# PushEventPayloadService.new(event, data).execute +class PushEventPayloadService + # event - The event this push payload belongs to. + # push_data - A Hash produced by `Gitlab::DataBuilder::Push.build` to use for + # building the push payload. + def initialize(event, push_data) + @event = event + @push_data = push_data + end + + # Creates and returns a new PushEventPayload row. + # + # This method will raise upon encountering validation errors. + # + # Returns an instance of PushEventPayload. + def execute + @event.build_push_event_payload( + commit_count: commit_count, + action: action, + ref_type: ref_type, + commit_from: commit_from_id, + commit_to: commit_to_id, + ref: trimmed_ref, + commit_title: commit_title, + event_id: @event.id + ) + + @event.push_event_payload.save! + @event.push_event_payload + end + + # Returns the commit title to use. + # + # The commit title is limited to the first line and a maximum of 70 + # characters. + def commit_title + commit = @push_data.fetch(:commits).last + + return nil unless commit && commit[:message] + + raw_msg = commit[:message] + + # Find where the first line ends, without turning the entire message into an + # Array of lines (this is a waste of memory for large commit messages). + index = raw_msg.index("\n") + message = index ? raw_msg[0..index] : raw_msg + + message.strip.truncate(70) + end + + def commit_from_id + if create? + nil + else + revision_before + end + end + + def commit_to_id + if remove? + nil + else + revision_after + end + end + + def commit_count + @push_data.fetch(:total_commits_count) + end + + def ref + @push_data.fetch(:ref) + end + + def revision_before + @push_data.fetch(:before) + end + + def revision_after + @push_data.fetch(:after) + end + + def trimmed_ref + Gitlab::Git.ref_name(ref) + end + + def create? + Gitlab::Git.blank_ref?(revision_before) + end + + def remove? + Gitlab::Git.blank_ref?(revision_after) + end + + def action + if create? + :created + elsif remove? + :removed + else + :pushed + end + end + + def ref_type + if Gitlab::Git.tag_ref?(ref) + :tag + else + :branch + end + end +end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index cbcd4478af6..a1c2f8d0180 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -4,7 +4,7 @@ class SystemHooksService end def execute_hooks(data, hooks_scope = :all) - SystemHook.public_send(hooks_scope).find_each do |hook| + SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, 'system_hooks') end end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 74ba814afff..4abd2c44b2f 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -18,7 +18,7 @@ module TestHooks end error_message = catch(:validation_error) do - sample_data = self.__send__(trigger_data_method) + sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend return hook.execute(sample_data, trigger) end diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index ef70871624b..3298ad104ec 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader end def self.base_dir - File.join(root_dir, 'system') + File.join(root_dir, '-', 'system') end private diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a4f49d3f6d7..8bf6556079b 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -48,6 +48,12 @@ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') %span.help-block#clone-protocol-help Allow only the selected protocols to be used for Git access. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :project_export_enabled do + = f.check_box :project_export_enabled + Project export enabled %fieldset %legend Account and Limit Settings diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml index 209afd4aab4..57544559824 100644 --- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -28,6 +28,6 @@ %h3.blank-state-title Create a group %p.blank-state-text - Groups are a great way to organise projects and people. + Groups are a great way to organize projects and people. = link_to new_group_path, class: "btn btn-new" do New group diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index ad434a64556..98cdcca3ecc 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -1,5 +1,5 @@ %li.commit .commit-row-title - = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit-sha", alt: '', title: truncate_sha(commit[:id]) + = link_to truncate_sha(event.commit_id), project_commit_path(project, event.commit_id), class: "commit-sha", alt: '', title: truncate_sha(event.commit_id) · - = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author + = markdown event_commit_title(event.commit_title), project: project, pipeline: :single_line, author: event.author diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 9fcacfbbf36..bf655f9d21a 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -1,14 +1,13 @@ %div{ xmlns: "http://www.w3.org/1999/xhtml" } - - event.commits.first(15).each do |commit| - %p - %strong= commit[:author][:name] - = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id]) - %i - at - = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author) - - if event.commits_count > 15 + %p + %strong= event.author_name + = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id) + %i + at + = event.created_at.to_s(:short) + %blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author) + - if event.commits_count > 1 %p %i \... and - = pluralize(event.commits_count - 15, "more commit") + = pluralize(event.commits_count - 1, "more commit") diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 54b414cc62a..973c652ad88 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -14,9 +14,7 @@ - if event.push_with_commits? .event-body %ul.well-list.event_commits - - few_commits = event.commits[0...2] - - few_commits.each do |commit| - = render "events/commit", commit: commit, project: project, event: event + = render "events/commit", project: project, event: event - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) - if event.commits_count > 1 @@ -44,9 +42,6 @@ = link_to create_mr_path(project.default_branch, event.ref_name, project) do Create Merge Request - elsif event.rm_ref? - - repository = project.repository - - last_commit = repository.commit(event.commit_from) - - if last_commit - .event-body - %ul.well-list.event_commits - = render "events/commit", commit: last_commit, project: project, event: event + .event-body + %ul.well-list.event_commits + = render "events/commit", project: project, event: event diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index f18c3a74120..445f0dffbcc 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ = icon('chevron-down') %ul.dropdown-menu %li - %a Sort by date + = link_to 'Sort by date', '#' = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index c52a515226e..84e0009487f 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -10,7 +10,7 @@ Customize how FogBugz email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import. %p - The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames wil be imported into GitLab. You can change this by populating the table below. + The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames will be imported into GitLab. You can change this by populating the table below. %ul %li %strong Default: Map a FogBugz account ID to a full name diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml new file mode 100644 index 00000000000..250029c4475 --- /dev/null +++ b/app/views/kaminari/gitlab/_without_count.html.haml @@ -0,0 +1,8 @@ +.gl-pagination + %ul.pagination.clearfix + - if previous_path + %li.prev + = link_to(t('views.pagination.previous'), previous_path, rel: 'prev') + - if next_path + %li.next + = link_to(t('views.pagination.next'), next_path, rel: 'next') diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index df1dc736571..b32cfe158bb 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -74,7 +74,8 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path - = render 'shared/user_dropdown_experimental_features' + %li + = link_to "Turn on new navigation", profile_preferences_path(anchor: "new-navigation") %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index fa94925d537..2c1c23d6ea9 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -68,7 +68,8 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path - = render 'shared/user_dropdown_experimental_features' + %li + = link_to "Turn off new navigation", profile_preferences_path(anchor: "new-navigation") %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml index 0b4a9d92bea..3cbcd841aff 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -1,150 +1,151 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to admin_root_path, title: 'Admin Overview' do - .avatar-container.s40.settings-avatar - = icon('wrench') - .project-title Admin Area - %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('overview') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Dashboard - = nav_link(controller: [:admin, :projects]) do - = link_to admin_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'jobs#index' do - = link_to admin_jobs_path, title: 'Jobs' do - %span - Jobs - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners - = nav_link path: 'cohorts#index' do - = link_to admin_cohorts_path, title: 'Cohorts' do - %span - Cohorts + .nav-sidebar-inner-scroll + .context-header + = link_to admin_root_path, title: 'Admin Overview' do + .avatar-container.s40.settings-avatar + = icon('wrench') + .project-title Admin Area + %ul.sidebar-top-level-items + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('overview') + %span.nav-item-name + Overview - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do - .nav-icon-container - = custom_icon('monitoring') - %span.nav-item-name - Monitoring + %ul.sidebar-sub-level-items + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Dashboard + = nav_link(controller: [:admin, :projects]) do + = link_to admin_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'jobs#index' do + = link_to admin_jobs_path, title: 'Jobs' do + %span + Jobs + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners + = nav_link path: 'cohorts#index' do + = link_to admin_cohorts_path, title: 'Cohorts' do + %span + Cohorts - %ul.sidebar-sub-level-items - = nav_link(controller: :conversational_development_index) do - = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do - %span - ConvDev Index - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do + .nav-icon-container + = custom_icon('monitoring') + %span.nav-item-name + Monitoring - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - .nav-icon-container - = custom_icon('messages') - %span.nav-item-name - Messages - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do - .nav-icon-container - = custom_icon('system_hooks') - %span.nav-item-name - System Hooks + %ul.sidebar-sub-level-items + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - .nav-icon-container - = custom_icon('applications') - %span.nav-item-name - Applications + = nav_link(controller: :broadcast_messages) do + = link_to admin_broadcast_messages_path, title: 'Messages' do + .nav-icon-container + = custom_icon('messages') + %span.nav-item-name + Messages + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to admin_hooks_path, title: 'Hooks' do + .nav-icon-container + = custom_icon('system_hooks') + %span.nav-item-name + System Hooks - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - .nav-icon-container - = custom_icon('abuse_reports') - %span.nav-item-name - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications - - if akismet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do .nav-icon-container - = custom_icon('spam_logs') + = custom_icon('abuse_reports') %span.nav-item-name - Spam Logs + Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - Deploy Keys + - if akismet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + .nav-icon-container + = custom_icon('spam_logs') + %span.nav-item-name + Spam Logs - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - .nav-icon-container - = custom_icon('service_templates') - %span.nav-item-name - Service Templates + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + Deploy Keys - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - .nav-icon-container - = custom_icon('labels') - %span.nav-item-name - Labels + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates' do + .nav-icon-container + = custom_icon('service_templates') + %span.nav-item-name + Service Templates - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - .nav-icon-container - = custom_icon('appearance') - %span.nav-item-name - Appearance + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels' do + .nav-icon-container + = custom_icon('labels') + %span.nav-item-name + Labels - %li.divider - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + .nav-icon-container + = custom_icon('appearance') + %span.nav-item-name + Appearance + + %li.divider + = nav_link(controller: :application_settings) do + = link_to admin_application_settings_path, title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index c7dabbd8237..ed5793f09fe 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -1,89 +1,90 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to group_path(@group), title: @group.name do - .avatar-container.s40.group-avatar - = image_tag group_icon(@group), class: "avatar s40 avatar-tile" - .group-title - = @group.name - %ul.sidebar-top-level-items - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group overview' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group details' do - %span - Details - - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity - - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - Issues - %span.badge.count= number_with_delimiter(issues.count) - - %ul.sidebar-sub-level-items - = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do - = link_to issues_group_path(@group), title: 'List' do - %span - List + .nav-sidebar-inner-scroll + .context-header + = link_to group_path(@group), title: @group.name do + .avatar-container.s40.group-avatar + = image_tag group_icon(@group), class: "avatar s40 avatar-tile" + .group-title + = @group.name + %ul.sidebar-top-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group overview' do + .nav-icon-container + = custom_icon('project') + %span.nav-item-name + Overview - = nav_link(path: 'labels#index') do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels + %ul.sidebar-sub-level-items + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group details' do + %span + Details - = nav_link(path: 'milestones#index') do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - Merge Requests - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do + = link_to issues_group_path(@group), title: 'Issues' do .nav-icon-container - = custom_icon('settings') + = custom_icon('issues') %span.nav-item-name - Settings + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + Issues + %span.badge.count= number_with_delimiter(issues.count) + %ul.sidebar-sub-level-items - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'General' do + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do %span - General + List - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do %span - Projects + Labels - = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do %span - CI / CD + Milestones + + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute + Merge Requests + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + - if current_user && can?(current_user, :admin_group, @group) + = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do + = link_to edit_group_path(@group), title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + %ul.sidebar-sub-level-items + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'General' do + %span + General + + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects + + = nav_link(controller: :ci_cd) do + = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + %span + CI / CD - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index edae009a28e..4234df56d1d 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -1,84 +1,85 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to profile_path, title: 'Profile Settings' do - .avatar-container.s40.settings-avatar - = icon('user') - .project-title User Settings - %ul.sidebar-top-level-items - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + .nav-sidebar-inner-scroll + .context-header = link_to profile_path, title: 'Profile Settings' do - .nav-icon-container - = custom_icon('profile') - %span.nav-item-name - Profile - = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do - .nav-icon-container - = custom_icon('account') - %span.nav-item-name - Account - - if current_application_settings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do + .avatar-container.s40.settings-avatar + = icon('user') + .project-title User Settings + %ul.sidebar-top-level-items + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + = link_to profile_path, title: 'Profile Settings' do .nav-icon-container - = custom_icon('applications') + = custom_icon('profile') %span.nav-item-name - Applications - = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do - .nav-icon-container - = custom_icon('chat') - %span.nav-item-name - Chat - = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - .nav-icon-container - = custom_icon('access_tokens') - %span.nav-item-name - Access Tokens - = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - .nav-icon-container - = custom_icon('emails') - %span.nav-item-name - Emails - - unless current_user.ldap_user? - = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do + Profile + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account' do .nav-icon-container - = custom_icon('lock') + = custom_icon('account') %span.nav-item-name - Password - = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - .nav-icon-container - = custom_icon('notifications') - %span.nav-item-name - Notifications + Account + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + .nav-icon-container + = custom_icon('chat') + %span.nav-item-name + Chat + = nav_link(controller: :personal_access_tokens) do + = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do + .nav-icon-container + = custom_icon('access_tokens') + %span.nav-item-name + Access Tokens + = nav_link(controller: :emails) do + = link_to profile_emails_path, title: 'Emails' do + .nav-icon-container + = custom_icon('emails') + %span.nav-item-name + Emails + - unless current_user.ldap_user? + = nav_link(controller: :passwords) do + = link_to edit_profile_password_path, title: 'Password' do + .nav-icon-container + = custom_icon('lock') + %span.nav-item-name + Password + = nav_link(controller: :notifications) do + = link_to profile_notifications_path, title: 'Notifications' do + .nav-icon-container + = custom_icon('notifications') + %span.nav-item-name + Notifications - = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - SSH Keys - = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do - .nav-icon-container - = custom_icon('key_2') - %span.nav-item-name - GPG Keys - = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do - .nav-icon-container - = custom_icon('preferences') - %span.nav-item-name - Preferences - = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do - .nav-icon-container - = custom_icon('authentication_log') - %span.nav-item-name - Authentication log + = nav_link(controller: :keys) do + = link_to profile_keys_path, title: 'SSH Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + SSH Keys + = nav_link(controller: :gpg_keys) do + = link_to profile_gpg_keys_path, title: 'GPG Keys' do + .nav-icon-container + = custom_icon('key_2') + %span.nav-item-name + GPG Keys + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences' do + .nav-icon-container + = custom_icon('preferences') + %span.nav-item-name + Preferences + = nav_link(path: 'profiles#audit_log') do + = link_to audit_log_profile_path, title: 'Authentication log' do + .nav-icon-container + = custom_icon('authentication_log') + %span.nav-item-name + Authentication log - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index e0477c29ebe..0ef81375c3a 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -1,261 +1,262 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - - can_edit = can?(current_user, :admin_project, @project) - .context-header - = link_to project_path(@project), title: @project.name do - .avatar-container.s40.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') - .project-title - = @project.name - %ul.sidebar-top-level-items - = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do - %span= _('Details') - - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do - %span= _('Activity') - - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(path: 'cycle_analytics#show') do - = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do - %span= _('Cycle Analytics') - - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-sidebar-inner-scroll + - can_edit = can?(current_user, :admin_project, @project) + .context-header + = link_to project_path(@project), title: @project.name do + .avatar-container.s40.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') + .project-title + = @project.name + %ul.sidebar-top-level-items + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do + = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do .nav-icon-container - = custom_icon('doc_text') + = custom_icon('project') %span.nav-item-name - Repository + Overview %ul.sidebar-sub-level-items - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_tree_path(@project) do - #{ _('Files') } - - = nav_link(controller: [:commit, :commits]) do - = link_to project_commits_path(@project, current_ref) do - #{ _('Commits') } - - = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do - #{ _('Branches') } - - = nav_link(controller: [:tags, :releases]) do - = link_to project_tags_path(@project) do - #{ _('Tags') } - - = nav_link(path: 'graphs#show') do - = link_to project_graph_path(@project, current_ref) do - #{ _('Contributors') } - - = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, current_ref) do - #{ s_('ProjectNetworkGraph|Graph') } - - = nav_link(controller: :compare) do - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - #{ _('Compare') } - - = nav_link(path: 'graphs#charts') do - = link_to charts_project_graph_path(@project, current_ref) do - #{ _('Charts') } - - - if project_nav_tab? :container_registry - = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - .nav-icon-container - = custom_icon('container_registry') - %span.nav-item-name - Registry - - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - Issues - - if @project.issues_enabled? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - - %ul.sidebar-sub-level-items - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), title: 'Issues' do - %span - List - - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do - %span - Board - - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels - - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones - - - if project_nav_tab? :merge_requests - = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do - .nav-icon-container - = custom_icon('pipeline') - %span.nav-item-name - CI / CD - - %ul.sidebar-sub-level-items - - if project_nav_tab? :pipelines - = nav_link(path: ['pipelines#index', 'pipelines#show']) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - - if project_nav_tab? :builds - = nav_link(controller: [:jobs, :artifacts]) do - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do + %span= _('Details') + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span= _('Activity') + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do + %span= _('Cycle Analytics') + + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do + = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('doc_text') + %span.nav-item-name + Repository + + %ul.sidebar-sub-level-items + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_tree_path(@project) do + #{ _('Files') } + + = nav_link(controller: [:commit, :commits]) do + = link_to project_commits_path(@project, current_ref) do + #{ _('Commits') } + + = nav_link(html_options: {class: branches_tab_class}) do + = link_to project_branches_path(@project) do + #{ _('Branches') } + + = nav_link(controller: [:tags, :releases]) do + = link_to project_tags_path(@project) do + #{ _('Tags') } + + = nav_link(path: 'graphs#show') do + = link_to project_graph_path(@project, current_ref) do + #{ _('Contributors') } + + = nav_link(controller: %w(network)) do + = link_to project_network_path(@project, current_ref) do + #{ s_('ProjectNetworkGraph|Graph') } + + = nav_link(controller: :compare) do + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do + #{ _('Compare') } + + = nav_link(path: 'graphs#charts') do + = link_to charts_project_graph_path(@project, current_ref) do + #{ _('Charts') } + + - if project_nav_tab? :container_registry + = nav_link(controller: %w[projects/registry/repositories]) do + = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + .nav-icon-container + = custom_icon('container_registry') + %span.nav-item-name + Registry + + - if project_nav_tab? :issues + = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do + = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name + Issues + - if @project.issues_enabled? + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + + %ul.sidebar-sub-level-items + = nav_link(controller: :issues) do + = link_to project_issues_path(@project), title: 'Issues' do %span - Jobs + List - - if project_nav_tab? :pipelines - = nav_link(controller: :pipeline_schedules) do - = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + = nav_link(controller: :boards) do + = link_to project_boards_path(@project), title: 'Board' do %span - Schedules + Board - - if project_nav_tab? :environments - = nav_link(controller: :environments) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: 'Labels' do %span - Environments + Labels - - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - = nav_link(path: 'pipelines#charts') do - = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: 'Milestones' do %span - Charts + Milestones + + - if project_nav_tab? :merge_requests + = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + Merge Requests + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do + = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do + .nav-icon-container + = custom_icon('pipeline') + %span.nav-item-name + CI / CD + + %ul.sidebar-sub-level-items + - if project_nav_tab? :pipelines + = nav_link(path: ['pipelines#index', 'pipelines#show']) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - .nav-icon-container - = custom_icon('wiki') - %span.nav-item-name - Wiki + - if project_nav_tab? :builds + = nav_link(controller: [:jobs, :artifacts]) do + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + %span + Jobs - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - .nav-icon-container - = custom_icon('snippets') - %span.nav-item-name - Snippets + - if project_nav_tab? :pipelines + = nav_link(controller: :pipeline_schedules) do + = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + %span + Schedules - - if project_nav_tab? :settings - = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + - if project_nav_tab? :environments + = nav_link(controller: :environments) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments - %ul.sidebar-sub-level-items - - can_edit = can?(current_user, :admin_project, @project) - - if can_edit - = nav_link(path: %w[projects#edit]) do - = link_to edit_project_path(@project), title: 'General' do - %span - General - = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: 'Members' do - %span - Members - - if can_edit - = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do - = link_to project_settings_integrations_path(@project), title: 'Integrations' do - %span - Integrations - = nav_link(controller: :repository) do - = link_to project_settings_repository_path(@project), title: 'Repository' do + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + %span + Charts + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + .nav-icon-container + = custom_icon('wiki') + %span.nav-item-name + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do + .nav-icon-container + = custom_icon('snippets') + %span.nav-item-name + Snippets + + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + + %ul.sidebar-sub-level-items + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(path: %w[projects#edit]) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :project_members) do + = link_to project_project_members_path(@project), title: 'Members' do %span - Repository - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + Members + - if can_edit + = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span - CI / CD - - if Gitlab.config.pages.enabled - = nav_link(controller: :pages) do - = link_to project_pages_path(@project), title: 'Pages' do + Integrations + = nav_link(controller: :repository) do + = link_to project_settings_repository_path(@project), title: 'Repository' do %span - Pages - - - else - = nav_link(path: %w[members#show]) do - = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - = render 'shared/sidebar_toggle_button' - - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + %span + CI / CD + - if Gitlab.config.pages.enabled + = nav_link(controller: :pages) do + = link_to project_pages_path(@project), title: 'Pages' do + %span + Pages + + - else + = nav_link(path: %w[members#show]) do + = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + + = render 'shared/sidebar_toggle_button' + + -# Shortcut to Project > Activity %li.hidden - = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do - Graph - - -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") - - unless @project.empty_repo? - %li.hidden - = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do - Charts - - -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue - - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + -# Shortcut to Repository > Graph (formerly, Network) + - if project_nav_tab? :network + %li.hidden + = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts + + -# Shortcut to Issues > New Issue %li.hidden - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do - Jobs - - -# Shortcut to commits page - - if project_nav_tab? :commits + = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do + Create a new issue + + -# Shortcut to Pipelines > Jobs + - if project_nav_tab? :builds + %li.hidden + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs + + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + + -# Shortcut to issue boards %li.hidden - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - Commits - - -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 9bd8bf91d1c..f08dcc0c242 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -18,8 +18,6 @@ = scheme.name .col-sm-12 %hr - %h3#experimental-features Experimental features - %hr .col-lg-4.profile-settings-sidebar#new-navigation %h4.prepend-top-0 New Navigation @@ -42,28 +40,6 @@ New .col-sm-12 %hr - .col-lg-4.profile-settings-sidebar#new-repository - %h4.prepend-top-0 - New Repository - %p - This setting allows you to turn on or off the new upcoming repository concept. - .col-lg-8.syntax-theme - .nav-wip - %p - The new repository is currently a work-in-progress concept and only usable on wide-screens. There are a number of improvements that we are working on in order to further refine the repository view. - %p - %a{ href: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/31890', target: 'blank' } Learn more - about the improvements that are coming soon! - = label_tag do - .preview= image_tag "old_repo.png" - %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_repo", checked: !show_new_repo? } - Old - = label_tag do - .preview= image_tag "new_repo.png" - %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_repo", checked: show_new_repo? } - New - .col-sm-12 - %hr .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Behavior diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml new file mode 100644 index 00000000000..623d3bc91c6 --- /dev/null +++ b/app/views/projects/_export.html.haml @@ -0,0 +1,41 @@ +- return unless current_application_settings.project_export_enabled? + +- project = local_assigns.fetch(:project) +- expanded = Rails.env.test? + +%section.settings + .settings-header + %h4 + Export project + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .bs-callout.bs-callout-info + %p.append-bottom-0 + %p + The following items will be exported: + %ul + %li Project and wiki repositories + %li Project uploads + %li Project configuration including web hooks and services + %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities + %p + The following items will NOT be exported: + %ul + %li Job traces and artifacts + %li LFS objects + %li Container registry images + %li CI variables + %li Any encrypted tokens + %p + Once the exported file is ready, you will receive a notification email with a download link. + - if project.export_project_path + = link_to 'Download export', download_export_project_path(project), + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" + = link_to 'Generate new export', generate_new_export_project_path(project), + method: :post, class: "btn btn-default" + - else + = link_to 'Export project', export_project_path(project), + method: :post, class: "btn btn-default" diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 6e13bf47ff6..97041b87c48 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,11 +1,9 @@ - referenced_users = local_assigns.fetch(:referenced_users, nil) - if defined?(@issue) && @issue.confidential? - %li.confidential-issue-warning + .confidential-issue-warning = confidential_icon(@issue) %span This is a confidential issue. Your comment will not be visible to the public. -- else - %li.confidential-issue-warning.not-confidential .md-area .md-header diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 21baf35f2ac..97cf13df070 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -5,6 +5,6 @@ Blank - Gitlab::ProjectTemplate.all.each do |template| .btn - %input{ type: "radio", autocomplete: "off", name: "project_templates", id: template.name } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } = custom_icon(template.logo) = template.title diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 32dbc1b3417..05b7dfe2872 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -19,7 +19,9 @@ = render 'shared/new_commit_form', placeholder: placeholder .form-actions - = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all' + = button_tag class: 'btn btn-create btn-upload-file', id: 'submit-all', type: 'button' do + = icon('spin spinner', class: 'js-loading-icon hidden' ) + = button_title = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 22674b671c9..83821326aec 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,3 +1,2 @@ - if commit.has_signature? %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } - %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index c704635ead3..3467e357c49 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -40,6 +40,9 @@ %i.fa.fa-chevron-down %ul.dropdown-menu.dropdown-menu-align-right %li + %a{ "href" => "#", "data-value" => "7" } + {{ n__('Last %d day', 'Last %d days', 7) }} + %li %a{ "href" => "#", "data-value" => "30" } {{ n__('Last %d day', 'Last %d days', 30) }} %li diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 8c8aa4c78f5..376f672f424 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,22 +2,24 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files +- merge_request = local_assigns.fetch(:merge_request, false) -.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed - .inline-parallel-buttons - - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } - = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' - - if show_whitespace_toggle - - if current_controller?(:commit) - = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') - - elsif current_controller?('projects/merge_requests/diffs') - = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') - - elsif current_controller?(:compare) - = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') - .btn-group - = inline_diff_btn - = parallel_diff_btn - = render 'projects/diffs/stats', diff_files: diff_files +.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) } + .files-changed-inner + .inline-parallel-buttons.hidden-xs.hidden-sm + - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } + = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' + - if show_whitespace_toggle + - if current_controller?(:commit) + = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') + - elsif current_controller?('projects/merge_requests/diffs') + = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') + - elsif current_controller?(:compare) + = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') + .btn-group + = inline_diff_btn + = parallel_diff_btn + = render 'projects/diffs/stats', diff_files: diff_files - if render_overflow_warning?(diff_files) = render 'projects/diffs/warning', diff_files: diffs diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index efc0ea31917..02fd54c97fb 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -10,7 +10,7 @@ %strong.cgreen #{sum_added_lines} additions and %strong.cred #{sum_removed_lines} deletions - .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" } + .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} %strong.cred< diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 20fceda26dc..6178abe9160 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -161,42 +161,7 @@ = render 'merge_request_settings', form: f = f.submit 'Save changes', class: "btn btn-save" - %section.settings - .settings-header - %h4 - Export project - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. - .settings-content.no-animate{ class: ('expanded' if expanded) } - .bs-callout.bs-callout-info - %p.append-bottom-0 - %p - The following items will be exported: - %ul - %li Project and wiki repositories - %li Project uploads - %li Project configuration including web hooks and services - %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities - %p - The following items will NOT be exported: - %ul - %li Job traces and artifacts - %li LFS objects - %li Container registry images - %li CI variables - %li Any encrypted tokens - %p - Once the exported file is ready, you will receive a notification email with a download link. - - if @project.export_project_path - = link_to 'Download export', download_export_project_path(@project), - rel: 'nofollow', download: '', method: :get, class: "btn btn-default" - = link_to 'Generate new export', generate_new_export_project_path(@project), - method: :post, class: "btn btn-default" - - else - = link_to 'Export project', export_project_path(@project), - method: :post, class: "btn btn-default" + = render 'export', project: @project %section.settings.advanced-settings .settings-header @@ -205,7 +170,7 @@ %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p - Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project. + Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project. .settings-content.no-animate{ class: ('expanded' if expanded) } .sub-section %h4 Housekeeping @@ -274,7 +239,7 @@ %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. - %li Project visibility level will be changed to match namespace rules when transfering to a group. + %li Project visibility level will be changed to match namespace rules when transferring to a group. = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - if @project.forked? && can?(current_user, :remove_fork_project, @project) .sub-section diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 756faf4625e..13809da6523 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,7 +1,7 @@ = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do = icon('rss') - if @can_bulk_update - = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" + = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = link_to "New issue", new_project_issue_path(@project, issue: { assignee_id: issues_finder.assignee.try(:id), milestone_id: issues_finder.milestones.first.try(:id) }), diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 99f4b30d085..f5d5bc7eda9 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -75,7 +75,7 @@ Pipeline = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit' from - = link_to "#{@build.pipeline.ref}", project_branch_path(@project, @build.pipeline.ref), class: 'link-commit' + = link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name' %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.stage-selection More = icon('chevron-down') diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index b787edb3427..3303aa72604 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -4,8 +4,8 @@ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"} - if @merge_request.reopenable? = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} - %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" } - %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 }} + %comment-and-resolve-btn{ "inline-template" => true } + %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", :autocomplete => true diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index e92f2712347..e73dab8ad4a 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -1,5 +1,5 @@ - if @can_bulk_update - = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" + = button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle" - if merge_project = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do New merge request diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml index fb31e2fef00..0d30d6da68f 100644 --- a/app/views/projects/merge_requests/diffs/_diffs.html.haml +++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/diffs/versions' - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index e3bbebbcf4c..647e0a772b1 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -25,7 +25,7 @@ .form-group = f.label :template_project, class: 'label-light' do Create from template - = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} + = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} %div = render 'project_templates', f: f .second-column diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 9c42be4e0ff..cb737d129f0 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -17,24 +17,32 @@ "inline-template" => true, "ref" => "note_#{note.id}" } - %button.note-action-button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - ":ref" => "'button'" } + .note-actions-item + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + ":ref" => "'button'" } - = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') - %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' + = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = 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') - %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + .note-actions-item + = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } 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') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') - = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable + - if note_editable + .note-actions-item + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + %span.link-highlight + = custom_icon('icon_pencil') + + = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 75a4687e1e3..5930209a682 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,14 +1,11 @@ - is_current_user = current_user == note.author - if note_editable || !is_current_user - .dropdown.more-actions + .dropdown.more-actions.note-actions-item = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do - = icon('ellipsis-v', class: 'icon') + %span.icon + = custom_icon('ellipsis_v') %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left - - if note_editable - %li - = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' - %li.divider - unless is_current_user %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 427b059cb82..853e2a6e7ec 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -2,8 +2,9 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path - if show_new_repo? - = icon('long-arrow-right', title: 'to target branch') - = render 'shared/target_switcher', destination: 'tree', path: @path + .tree-ref-target-holder.js-tree-ref-target-holder + = icon('long-arrow-right', title: 'to target branch') + = render 'shared/target_switcher', destination: 'tree', path: @path - unless show_new_repo? = render 'projects/tree/old_tree_header' diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4498c8f8349..7ad743b3b81 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -6,7 +6,7 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } = dropdown_title _("Switch branch/tag") = dropdown_filter _("Search branches and tags") diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml index 3672b552f10..9236868652f 100644 --- a/app/views/shared/_target_switcher.html.haml +++ b/app/views/shared/_target_switcher.html.haml @@ -1,5 +1,5 @@ - dropdown_toggle_text = @ref || @project.default_branch -= form_tag nil, method: :get, class: "project-refs-target-form" do += form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path diff --git a/app/views/shared/_user_dropdown_experimental_features.html.haml b/app/views/shared/_user_dropdown_experimental_features.html.haml deleted file mode 100644 index 8e71407b748..00000000000 --- a/app/views/shared/_user_dropdown_experimental_features.html.haml +++ /dev/null @@ -1 +0,0 @@ -%li= link_to 'Experimental features', profile_preferences_path(anchor: 'experimental-features') diff --git a/app/views/shared/icons/_ellipsis_v.svg b/app/views/shared/icons/_ellipsis_v.svg new file mode 100644 index 00000000000..9117a9bb9ec --- /dev/null +++ b/app/views/shared/icons/_ellipsis_v.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1600"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></svg> diff --git a/app/views/shared/icons/_node_express.svg b/app/views/shared/icons/_express.svg index f2c94319f19..f2c94319f19 100644 --- a/app/views/shared/icons/_node_express.svg +++ b/app/views/shared/icons/_express.svg diff --git a/app/views/shared/icons/_java_spring.svg b/app/views/shared/icons/_spring.svg index 508349aa456..508349aa456 100644 --- a/app/views/shared/icons/_java_spring.svg +++ b/app/views/shared/icons/_spring.svg diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c2de6926460..c3f25c9d255 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('sidebar') -%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix", signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { "offset-top" => ("50" unless show_new_nav?), "spy" => ("affix" unless show_new_nav?), signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6f6a036b13f..6a85f7d0564 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -32,7 +32,7 @@ .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) - if milestone.is_group_milestone? - = link_to edit_group_milestone_path(@group, milestone.id), class: "btn btn-xs btn-grouped" do + = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do Edit \ - if milestone.closed? diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 66ac8196f2f..40379f48393 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -1,7 +1,7 @@ - affix_offset = local_assigns.fetch(:affix_offset, "50") - project = local_assigns[:project] -%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar.milestone-sidebar .block.milestone-progress.issuable-sidebar-header %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index b93837e3087..3014300fbe7 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -23,7 +23,7 @@ .pull-right - if can?(current_user, :admin_milestones, group) - if milestone.is_group_milestone? - = link_to edit_group_milestone_path(group, milestone.iid), class: "btn btn btn-grouped" do + = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do Edit - if milestone.active? = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 914506bf0ce..0bedfea3502 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -23,6 +23,6 @@ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') %strong= pluralize(@private_forks_count, 'private fork') %span you have no access to. - = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages + = paginate_collection(projects, remote: remote) - else .nothing-here-block No projects found diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 0fc40cf0801..87fa2007d16 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,2 +1,7 @@ -#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } } - %repo +#repo{ data: { url: content_url, + project_name: project.name, + refs_url: refs_project_path(project, format: :json), + project_url: project_path(project), + project_id: project.id, + can_commit: (!!can_push_branch?(project, @ref)).to_s, + on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index 098a88c48c5..3a50324770d 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,10 +1,17 @@ - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = 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') - %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + .note-actions-item + = 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') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + + - if note_editable + .note-actions-item + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + %span.link-highlight + = custom_icon('icon_pencil') = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb index 3fd472bf0c1..eb0d6c9c36c 100644 --- a/app/workers/concerns/new_issuable.rb +++ b/app/workers/concerns/new_issuable.rb @@ -1,20 +1,23 @@ module NewIssuable attr_reader :issuable, :user - def ensure_objects_found(issuable_id, user_id) - @issuable = issuable_class.find_by(id: issuable_id) - unless @issuable - log_error(issuable_class, issuable_id) - return false - end + def objects_found?(issuable_id, user_id) + set_user(user_id) + set_issuable(issuable_id) + + user && issuable + end + def set_user(user_id) @user = User.find_by(id: user_id) - unless @user - log_error(User, user_id) - return false - end - true + log_error(User, user_id) unless @user + end + + def set_issuable(issuable_id) + @issuable = issuable_class.find_by(id: issuable_id) + + log_error(issuable_class, issuable_id) unless @issuable end def log_error(record_class, record_id) diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index 4f47717ff69..f34dff2d656 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -4,13 +4,9 @@ class CreateGpgSignatureWorker def perform(commit_sha, project_id) project = Project.find_by(id: project_id) - return unless project - commit = project.commit(commit_sha) - - return unless commit - - commit.signature + # This calculates and caches the signature in the database + Gitlab::Gpg::Commit.new(project, commit_sha).signature end end diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 964287a1793..0ec871e00e1 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -4,6 +4,6 @@ class GitlabShellWorker include DedicatedSidekiqQueue def perform(action, *arg) - gitlab_shell.send(action, *arg) + gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb index bfae0c77700..a9073742ff7 100644 --- a/app/workers/namespaceless_project_destroy_worker.rb +++ b/app/workers/namespaceless_project_destroy_worker.rb @@ -24,10 +24,6 @@ class NamespacelessProjectDestroyWorker unlink_fork(project) if project.forked? - # Override Project#remove_pages for this instance so it doesn't do anything - def project.remove_pages - end - project.destroy! end diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb index 19a778ad522..d9a8e892e90 100644 --- a/app/workers/new_issue_worker.rb +++ b/app/workers/new_issue_worker.rb @@ -4,7 +4,7 @@ class NewIssueWorker include NewIssuable def perform(issue_id, user_id) - return unless ensure_objects_found(issue_id, user_id) + return unless objects_found?(issue_id, user_id) EventCreateService.new.open_issue(issuable, user) NotificationService.new.new_issue(issuable, user) diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb index 3c8a68016ff..1910c490159 100644 --- a/app/workers/new_merge_request_worker.rb +++ b/app/workers/new_merge_request_worker.rb @@ -4,7 +4,7 @@ class NewMergeRequestWorker include NewIssuable def perform(merge_request_id, user_id) - return unless ensure_objects_found(merge_request_id, user_id) + return unless objects_found?(merge_request_id, user_id) EventCreateService.new.open_mr(issuable, user) NotificationService.new.new_merge_request(issuable, user) diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 4eeb9666bb0..64788da7299 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -4,7 +4,7 @@ class PagesWorker sidekiq_options queue: :pages, retry: false def perform(action, *arg) - send(action, *arg) + send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend end def deploy(build_id) diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index a338523dc6b..cde5b45ad41 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -5,14 +5,17 @@ class RepositoryForkWorker include Gitlab::ShellAdapter include DedicatedSidekiqQueue + sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION + def perform(project_id, forked_from_repository_storage_path, source_path, target_path) + project = Project.find(project_id) + + return unless start_fork(project) + Gitlab::Metrics.add_event(:fork_repository, source_path: source_path, target_path: target_path) - project = Project.find(project_id) - project.import_start - result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, project.repository_storage_path, target_path) raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result @@ -33,6 +36,13 @@ class RepositoryForkWorker private + def start_fork(project) + return true if project.import_start + + Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.") + false + end + def fail_fork(project, message) Rails.logger.error(message) project.mark_import_as_failed(message) diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 6be541abd3e..2c2d1e8b91f 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -4,23 +4,18 @@ class RepositoryImportWorker include Sidekiq::Worker include DedicatedSidekiqQueue - sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION - - attr_accessor :project, :current_user + sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION def perform(project_id) - @project = Project.find(project_id) - @current_user = @project.creator + project = Project.find(project_id) - project.import_start + return unless start_import(project) Gitlab::Metrics.add_event(:import_repository, - import_url: @project.import_url, - path: @project.full_path) - - project.update_columns(import_jid: self.jid, import_error: nil) + import_url: project.import_url, + path: project.full_path) - result = Projects::ImportService.new(project, current_user).execute + result = Projects::ImportService.new(project, project.creator).execute raise ImportError, result[:message] if result[:status] == :error project.repository.after_import @@ -37,6 +32,13 @@ class RepositoryImportWorker private + def start_import(project) + return true if project.import_start + + Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") + false + end + def fail_import(project, message) project.mark_import_as_failed(message) end diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb index bfc5e667bb6..f850e459cd9 100644 --- a/app/workers/stuck_import_jobs_worker.rb +++ b/app/workers/stuck_import_jobs_worker.rb @@ -2,36 +2,60 @@ class StuckImportJobsWorker include Sidekiq::Worker include CronjobQueue - IMPORT_EXPIRATION = 15.hours.to_i + IMPORT_JOBS_EXPIRATION = 15.hours.to_i def perform - stuck_projects.find_in_batches(batch_size: 500) do |group| + projects_without_jid_count = mark_projects_without_jid_as_failed! + projects_with_jid_count = mark_projects_with_jid_as_failed! + + Gitlab::Metrics.add_event(:stuck_import_jobs, + projects_without_jid_count: projects_without_jid_count, + projects_with_jid_count: projects_with_jid_count) + end + + private + + def mark_projects_without_jid_as_failed! + started_projects_without_jid.each do |project| + project.mark_import_as_failed(error_message) + end.count + end + + def mark_projects_with_jid_as_failed! + completed_jids_count = 0 + + started_projects_with_jid.find_in_batches(batch_size: 500) do |group| jids = group.map(&:import_jid) # Find the jobs that aren't currently running or that exceeded the threshold. - completed_jids = Gitlab::SidekiqStatus.completed_jids(jids) + completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set if completed_jids.any? - completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id) + completed_jids_count += completed_jids.count + group.each do |project| + project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid) + end - fail_batch!(completed_jids, completed_ids) + Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}") end end - end - private + completed_jids_count + end - def stuck_projects - Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil) + def started_projects + Project.with_import_status(:started) end - def fail_batch!(completed_jids, completed_ids) - Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message) + def started_projects_with_jid + started_projects.where.not(import_jid: nil) + end - Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}") + def started_projects_without_jid + started_projects.where(import_jid: nil) end def error_message - "Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds" + "Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds" end end diff --git a/bin/changelog b/bin/changelog index 4c894f8ff5b..61d4de06e90 100755 --- a/bin/changelog +++ b/bin/changelog @@ -14,54 +14,107 @@ Options = Struct.new( :dry_run, :force, :merge_request, - :title + :title, + :type ) +INVALID_TYPE = -1 class ChangelogOptionParser - def self.parse(argv) - options = Options.new + Type = Struct.new(:name, :description) + TYPES = [ + Type.new('added', 'New feature'), + Type.new('fixed', 'Bug fix'), + Type.new('changed', 'Feature change'), + Type.new('deprecated', 'New deprecation'), + Type.new('removed', 'Feature removal'), + Type.new('security', 'Security fix'), + Type.new('other', 'Other') + ].freeze + TYPES_OFFSET = 1 + + class << self + def parse(argv) + options = Options.new + + parser = OptionParser.new do |opts| + opts.banner = "Usage: #{__FILE__} [options] [title]\n\n" + + # Note: We do not provide a shorthand for this in order to match the `git + # commit` interface + opts.on('--amend', 'Amend the previous commit') do |value| + options.amend = value + end + + opts.on('-f', '--force', 'Overwrite an existing entry') do |value| + options.force = value + end + + opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value| + options.merge_request = value + end + + opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value| + options.dry_run = value + end + + opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value| + options.author = git_user_name if value + end + + opts.on('-t', '--type [string]', String, "The category of the change, valid options are: #{TYPES.map(&:name).join(', ')}") do |value| + options.type = parse_type(value) + end + + opts.on('-h', '--help', 'Print help message') do + $stdout.puts opts + exit + end + end - parser = OptionParser.new do |opts| - opts.banner = "Usage: #{__FILE__} [options] [title]\n\n" + parser.parse!(argv) - # Note: We do not provide a shorthand for this in order to match the `git - # commit` interface - opts.on('--amend', 'Amend the previous commit') do |value| - options.amend = value - end + # Title is everything that remains, but let's clean it up a bit + options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '') - opts.on('-f', '--force', 'Overwrite an existing entry') do |value| - options.force = value - end + options + end - opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value| - options.merge_request = value - end + def read_type + read_type_message - opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value| - options.dry_run = value - end + type = TYPES[$stdin.getc.to_i - TYPES_OFFSET] + assert_valid_type!(type) - opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value| - options.author = git_user_name if value - end + type.name + end + + private - opts.on('-h', '--help', 'Print help message') do - $stdout.puts opts - exit + def parse_type(name) + type_found = TYPES.find do |type| + type.name == name end + type_found ? type_found.name : INVALID_TYPE end - parser.parse!(argv) - - # Title is everything that remains, but let's clean it up a bit - options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '') + def read_type_message + $stdout.puts "\n>> Please specify the index for the category of your change:" + TYPES.each_with_index do |type, index| + $stdout.puts "#{index + TYPES_OFFSET}. #{type.description}" + end + $stdout.print "\n?> " + end - options - end + def assert_valid_type!(type) + unless type + $stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}" + exit 1 + end + end - def self.git_user_name - %x{git config user.name}.strip + def git_user_name + %x{git config user.name}.strip + end end end @@ -72,8 +125,12 @@ class ChangelogEntry @options = options assert_feature_branch! - assert_new_file! assert_title! + assert_new_file! + + # Read type from $stdin unless is already set + options.type ||= ChangelogOptionParser.read_type + assert_valid_type! $stdout.puts "\e[32mcreate\e[0m #{file_path}" $stdout.puts contents @@ -90,7 +147,8 @@ class ChangelogEntry yaml_content = YAML.dump( 'title' => title, 'merge_request' => options.merge_request, - 'author' => options.author + 'author' => options.author, + 'type' => options.type ) remove_trailing_whitespace(yaml_content) end @@ -129,6 +187,12 @@ class ChangelogEntry " to use the title from the previous commit." end + def assert_valid_type! + return unless options.type && options.type == INVALID_TYPE + + fail_with 'Invalid category given!' + end + def title if options.title.empty? last_commit_subject diff --git a/bin/rspec-stackprof b/bin/rspec-stackprof index df79feb201d..810863ea4a0 100755 --- a/bin/rspec-stackprof +++ b/bin/rspec-stackprof @@ -1,5 +1,6 @@ #!/usr/bin/env ruby +require 'bundler/setup' require 'stackprof' $:.unshift 'spec' require 'rails_helper' @@ -13,4 +14,4 @@ StackProf.run(mode: :wall, out: output_file, interval: interval) do RSpec::Core::Runner.run(ARGV, $stderr, $stdout) end -system("stackprof #{output_file} --text --limit #{limit}") +system("bundle exec stackprof #{output_file} --text --limit #{limit}") diff --git a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml new file mode 100644 index 00000000000..90b169390d2 --- /dev/null +++ b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml @@ -0,0 +1,6 @@ +--- +title: disabling notifications globally now properly turns off group/project added + emails +merge_request: 13325 +author: @jneen +type: fixed diff --git a/changelogs/unreleased/21949-add-type-to-changelog.yml b/changelogs/unreleased/21949-add-type-to-changelog.yml new file mode 100644 index 00000000000..a20f6b7ad4e --- /dev/null +++ b/changelogs/unreleased/21949-add-type-to-changelog.yml @@ -0,0 +1,4 @@ +--- +title: Added type to CHANGELOG entries +merge_request: +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/29811-fix-line-number-alignment.yml b/changelogs/unreleased/29811-fix-line-number-alignment.yml new file mode 100644 index 00000000000..94b3328a7f2 --- /dev/null +++ b/changelogs/unreleased/29811-fix-line-number-alignment.yml @@ -0,0 +1,4 @@ +--- +title: Fix the alignment of line numbers to lines of code in code viewer +merge_request: 13403 +author: Trevor Flynn
\ No newline at end of file diff --git a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml new file mode 100644 index 00000000000..278ef2a8acb --- /dev/null +++ b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml @@ -0,0 +1,4 @@ +--- +title: Added tests for commits API unauthenticated user and public/private project +merge_request: 13287 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/34339-user_avatar-url-in-push-event-webhook-json-payload-is-relative-should-be-absolute.yml b/changelogs/unreleased/34339-user_avatar-url-in-push-event-webhook-json-payload-is-relative-should-be-absolute.yml new file mode 100644 index 00000000000..13f28da8577 --- /dev/null +++ b/changelogs/unreleased/34339-user_avatar-url-in-push-event-webhook-json-payload-is-relative-should-be-absolute.yml @@ -0,0 +1,4 @@ +--- +title: Use full path of user's avatar in webhooks +merge_request: 13401 +author: Vitaliy @blackst0ne Klachkov diff --git a/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml b/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml new file mode 100644 index 00000000000..7de30d82601 --- /dev/null +++ b/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml @@ -0,0 +1,6 @@ +--- +title: Improves performance of vue code by using vue files and moving svg out of data + function in pipeline schedule callout +merge_request: +author: +type: other diff --git a/changelogs/unreleased/34492-firefox-job.yml b/changelogs/unreleased/34492-firefox-job.yml deleted file mode 100644 index 881b8f649ea..00000000000 --- a/changelogs/unreleased/34492-firefox-job.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use jQuery to control scroll behavior in job log for cross browser consistency -merge_request: -author: diff --git a/changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml b/changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml new file mode 100644 index 00000000000..08171f6bcec --- /dev/null +++ b/changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: move edit comment button outside of dropdown +merge_request: +author: diff --git a/changelogs/unreleased/34533-speed-up-group-project-authorizations.yml b/changelogs/unreleased/34533-speed-up-group-project-authorizations.yml new file mode 100644 index 00000000000..ddaaf4a2507 --- /dev/null +++ b/changelogs/unreleased/34533-speed-up-group-project-authorizations.yml @@ -0,0 +1,5 @@ +--- +title: Fix timeouts when creating projects in groups with many members +merge_request: 13508 +author: +type: fixed diff --git a/changelogs/unreleased/34643-fix-project-path-slugify.yml b/changelogs/unreleased/34643-fix-project-path-slugify.yml new file mode 100644 index 00000000000..f7018a1aca5 --- /dev/null +++ b/changelogs/unreleased/34643-fix-project-path-slugify.yml @@ -0,0 +1,4 @@ +--- +title: Fix CI_PROJECT_PATH_SLUG slugify +merge_request: 13350 +author: Ivan Chernov diff --git a/changelogs/unreleased/35072-fix-pages-delete.yml b/changelogs/unreleased/35072-fix-pages-delete.yml new file mode 100644 index 00000000000..21af2bde201 --- /dev/null +++ b/changelogs/unreleased/35072-fix-pages-delete.yml @@ -0,0 +1,5 @@ +--- +title: Fix deleting GitLab Pages files when a project is removed +merge_request: 13631 +author: +type: fixed diff --git a/changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml b/changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml new file mode 100644 index 00000000000..3cdb3011f5b --- /dev/null +++ b/changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml @@ -0,0 +1,4 @@ +--- +title: Raise guessed encoding confidence threshold to 50 +merge_request: 12990 +author: diff --git a/changelogs/unreleased/35232-next-unresolved.yml b/changelogs/unreleased/35232-next-unresolved.yml deleted file mode 100644 index 45f3fb429a8..00000000000 --- a/changelogs/unreleased/35232-next-unresolved.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fix jump to next discussion button -merge_request: -author: diff --git a/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml b/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml deleted file mode 100644 index 54b2e71bef9..00000000000 --- a/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow any logged in users to read_users_list even if it's restricted -merge_request: 13201 -author: diff --git a/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml b/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml new file mode 100644 index 00000000000..04791e09b84 --- /dev/null +++ b/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml @@ -0,0 +1,4 @@ +--- +title: Add checks for branch existence before changing HEAD +merge_request: 13359 +author: Vitaliy @blackst0ne Klachkov diff --git a/changelogs/unreleased/36119-issuable-workers.yml b/changelogs/unreleased/36119-issuable-workers.yml new file mode 100644 index 00000000000..beb01ae5b1a --- /dev/null +++ b/changelogs/unreleased/36119-issuable-workers.yml @@ -0,0 +1,4 @@ +--- +title: Simplify checking if objects exist code in new issaubles workers +merge_request: +author: diff --git a/changelogs/unreleased/36185-or-separator.yml b/changelogs/unreleased/36185-or-separator.yml new file mode 100644 index 00000000000..4e46e60ea1b --- /dev/null +++ b/changelogs/unreleased/36185-or-separator.yml @@ -0,0 +1,4 @@ +--- +title: Align OR separator to center in new project page +merge_request: +author: diff --git a/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml b/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml new file mode 100644 index 00000000000..b51b5e58b39 --- /dev/null +++ b/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml @@ -0,0 +1,6 @@ +--- +title: Include the `is_admin` field in the `GET /users/:id` API when current user + is an admin +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/36385-pipeline-graph-dropdown.yml b/changelogs/unreleased/36385-pipeline-graph-dropdown.yml new file mode 100644 index 00000000000..1a43c66debd --- /dev/null +++ b/changelogs/unreleased/36385-pipeline-graph-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Prevents jobs dropdown from closing in pipeline graph +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/appearances-caching-and-schema.yml b/changelogs/unreleased/appearances-caching-and-schema.yml new file mode 100644 index 00000000000..5743f6e0f2d --- /dev/null +++ b/changelogs/unreleased/appearances-caching-and-schema.yml @@ -0,0 +1,4 @@ +--- +title: Cache Appearance instances in Redis +merge_request: +author: diff --git a/changelogs/unreleased/broadcast-messages-cache.yml b/changelogs/unreleased/broadcast-messages-cache.yml new file mode 100644 index 00000000000..a3c9e1ff465 --- /dev/null +++ b/changelogs/unreleased/broadcast-messages-cache.yml @@ -0,0 +1,4 @@ +--- +title: Better caching and indexing of broadcast messages +merge_request: +author: diff --git a/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml b/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml new file mode 100644 index 00000000000..7571999fa75 --- /dev/null +++ b/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml @@ -0,0 +1,4 @@ +--- +title: Bumps omniauth-ldap gem version to 2.0.4 +merge_request: 13465 +author: diff --git a/changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml b/changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml new file mode 100644 index 00000000000..a24cc7a1c43 --- /dev/null +++ b/changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml @@ -0,0 +1,4 @@ +--- +title: Don't rename namespace called system when upgrading from 9.1.x to 9.5 +merge_request: 13228 +author: diff --git a/changelogs/unreleased/commits-list-page-limit.yml b/changelogs/unreleased/commits-list-page-limit.yml new file mode 100644 index 00000000000..2fd54c5960a --- /dev/null +++ b/changelogs/unreleased/commits-list-page-limit.yml @@ -0,0 +1,5 @@ +--- +title: Fix commit list not loading the correct page when scrolling +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/disable-project-export.yml b/changelogs/unreleased/disable-project-export.yml new file mode 100644 index 00000000000..d7ca9f46193 --- /dev/null +++ b/changelogs/unreleased/disable-project-export.yml @@ -0,0 +1,4 @@ +--- +title: Add option to disable project export on instance +merge_request: 13211 +author: Robin Bobbitt diff --git a/changelogs/unreleased/fix-btn-alignment.yml b/changelogs/unreleased/fix-btn-alignment.yml new file mode 100644 index 00000000000..e5dce3d3a0e --- /dev/null +++ b/changelogs/unreleased/fix-btn-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Fix inconsistent spacing for edit buttons on issues and merge request page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-edit-merge-request-button-case.yml b/changelogs/unreleased/fix-edit-merge-request-button-case.yml new file mode 100644 index 00000000000..8550f3e3c1b --- /dev/null +++ b/changelogs/unreleased/fix-edit-merge-request-button-case.yml @@ -0,0 +1,5 @@ +--- +title: Fix edit merge request and issues button inconsistent letter casing +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml b/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml deleted file mode 100644 index 1558e575e6d..00000000000 --- a/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix links to group milestones from issue and merge request sidebar -merge_request: -author: diff --git a/changelogs/unreleased/fix-import-symbolink-links.yml b/changelogs/unreleased/fix-import-symbolink-links.yml new file mode 100644 index 00000000000..36e73821bdc --- /dev/null +++ b/changelogs/unreleased/fix-import-symbolink-links.yml @@ -0,0 +1,4 @@ +--- +title: Remove hidden symlinks from project import files +merge_request: +author: diff --git a/changelogs/unreleased/fix-oauth-checkboxes.yml b/changelogs/unreleased/fix-oauth-checkboxes.yml deleted file mode 100644 index 2839ccc42cb..00000000000 --- a/changelogs/unreleased/fix-oauth-checkboxes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed sign-in restrictions buttons not toggling active state -merge_request: -author: diff --git a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml b/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml deleted file mode 100644 index ddaec4f19f9..00000000000 --- a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix an order of operations for CI connection error message in merge request - widget -merge_request: 13252 -author: diff --git a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml deleted file mode 100644 index 07840205b6e..00000000000 --- a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline_schedules pages when active schedule has an abnormal state -merge_request: 13286 -author: diff --git a/changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml b/changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml new file mode 100644 index 00000000000..66b5b6b4f47 --- /dev/null +++ b/changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml @@ -0,0 +1,4 @@ +--- +title: Make GPGME temporary directory handling thread safe +merge_request: 13481 +author: Alexis Reigel diff --git a/changelogs/unreleased/forks-count-cache.yml b/changelogs/unreleased/forks-count-cache.yml new file mode 100644 index 00000000000..da8c53c2abd --- /dev/null +++ b/changelogs/unreleased/forks-count-cache.yml @@ -0,0 +1,5 @@ +--- +title: Cache the number of forks of a project +merge_request: 13535 +author: +type: other diff --git a/changelogs/unreleased/issue_31790.yml b/changelogs/unreleased/issue_31790.yml new file mode 100644 index 00000000000..df02cad423a --- /dev/null +++ b/changelogs/unreleased/issue_31790.yml @@ -0,0 +1,4 @@ +--- +title: Fix API responses when dealing with txt files +merge_request: +author: diff --git a/changelogs/unreleased/issue_35580.yml b/changelogs/unreleased/issue_35580.yml new file mode 100644 index 00000000000..3a94e771e25 --- /dev/null +++ b/changelogs/unreleased/issue_35580.yml @@ -0,0 +1,4 @@ +--- +title: Fix project milestones import when projects belongs to a group +merge_request: +author: diff --git a/changelogs/unreleased/mattermost_fixes.yml b/changelogs/unreleased/mattermost_fixes.yml deleted file mode 100644 index 667109a0bb4..00000000000 --- a/changelogs/unreleased/mattermost_fixes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Mattermost integration -merge_request: -author: diff --git a/changelogs/unreleased/migrate-events-into-a-new-format.yml b/changelogs/unreleased/migrate-events-into-a-new-format.yml new file mode 100644 index 00000000000..8a29f75323f --- /dev/null +++ b/changelogs/unreleased/migrate-events-into-a-new-format.yml @@ -0,0 +1,4 @@ +--- +title: Migrate events into a new format to reduce the storage necessary and improve performance +merge_request: +author: diff --git a/changelogs/unreleased/mk-fix-deploy-key-deletion.yml b/changelogs/unreleased/mk-fix-deploy-key-deletion.yml deleted file mode 100644 index 9ff2e49b14c..00000000000 --- a/changelogs/unreleased/mk-fix-deploy-key-deletion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix deletion of deploy keys linked to other projects -merge_request: 13162 -author: diff --git a/changelogs/unreleased/pagination-projects-explore.yml b/changelogs/unreleased/pagination-projects-explore.yml new file mode 100644 index 00000000000..dc9c4218793 --- /dev/null +++ b/changelogs/unreleased/pagination-projects-explore.yml @@ -0,0 +1,4 @@ +--- +title: Use Prev/Next pagination for exploring projects +merge_request: +author: diff --git a/changelogs/unreleased/project-foreign-keys-without-errors.yml b/changelogs/unreleased/project-foreign-keys-without-errors.yml deleted file mode 100644 index 63c53c8ad8f..00000000000 --- a/changelogs/unreleased/project-foreign-keys-without-errors.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change project FK migration to skip existing FKs -merge_request: -author: diff --git a/changelogs/unreleased/rs-alphanumeric-ssh-params.yml b/changelogs/unreleased/rs-alphanumeric-ssh-params.yml new file mode 100644 index 00000000000..426b01cafad --- /dev/null +++ b/changelogs/unreleased/rs-alphanumeric-ssh-params.yml @@ -0,0 +1,5 @@ +--- +title: Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric + character +merge_request: +author: diff --git a/changelogs/unreleased/search-flickering.yml b/changelogs/unreleased/search-flickering.yml deleted file mode 100644 index 951a5a0292a..00000000000 --- a/changelogs/unreleased/search-flickering.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix search box losing focus when typing -merge_request: -author: diff --git a/changelogs/unreleased/seven-days-cycle-analytics.yml b/changelogs/unreleased/seven-days-cycle-analytics.yml new file mode 100644 index 00000000000..ff660bdd603 --- /dev/null +++ b/changelogs/unreleased/seven-days-cycle-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Add a `Last 7 days` option for Cycle Analytics view +merge_request: 13443 +author: Mehdi Lahmam (@mehlah) +type: added diff --git a/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml b/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml deleted file mode 100644 index 9ca5f81cf79..00000000000 --- a/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make Delete Merged Branches handle wildcard protected branches correctly -merge_request: 13251 -author: diff --git a/changelogs/unreleased/use-a-specialized-class-for-querying-events.yml b/changelogs/unreleased/use-a-specialized-class-for-querying-events.yml new file mode 100644 index 00000000000..6c1ec10aa12 --- /dev/null +++ b/changelogs/unreleased/use-a-specialized-class-for-querying-events.yml @@ -0,0 +1,4 @@ +--- +title: Use a specialized class for querying events to improve performance +merge_request: +author: diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml new file mode 100644 index 00000000000..daa6a234c07 --- /dev/null +++ b/changelogs/unreleased/zj-upgrade-grape.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade grape to 1.0 +merge_request: +author: +type: other diff --git a/config/application.rb b/config/application.rb index 47887bf8596..f69dab4de39 100644 --- a/config/application.rb +++ b/config/application.rb @@ -176,7 +176,7 @@ module Gitlab next unless name.include?('namespace_project') define_method(name.sub('namespace_project', 'project')) do |project, *args| - send(name, project&.namespace, project, *args) + send(name, project&.namespace, project, *args) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e73db08fcac..25285525846 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -649,6 +649,9 @@ test: default: path: tmp/tests/repositories/ gitaly_address: unix:tmp/tests/gitaly/gitaly.socket + failure_count_threshold: 999999 + failure_wait_time: 0 + storage_timeout: 30 broken: path: tmp/tests/non-existent-repositories gitaly_address: unix:tmp/tests/gitaly/gitaly.socket diff --git a/config/initializers/0_acts_as_taggable.rb b/config/initializers/0_acts_as_taggable.rb index 54e9fcc31db..50dc47673ab 100644 --- a/config/initializers/0_acts_as_taggable.rb +++ b/config/initializers/0_acts_as_taggable.rb @@ -5,5 +5,5 @@ ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.tags_counter = false # validate that counter cache is disabled -raise "Counter cache is not disabled" if +raise "Counter cache is not disabled" if ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache] diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 2699173fc61..38ade18bdc0 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,3 +1,5 @@ +# rubocop:disable GitlabSecurity/PublicSend + require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible class Settings < Settingslogic @@ -71,7 +73,7 @@ class Settings < Settingslogic # check that `current` (string or integer) is a contant in `modul`. def verify_constant(modul, current, default) - constant = modul.constants.find{ |name| modul.const_get(name) == current } + constant = modul.constants.find { |name| modul.const_get(name) == current } value = constant.nil? ? default : modul.const_get(constant) if current.is_a? String value = modul.const_get(current.upcase) rescue default diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index 92ce4dd03cd..f8e67ce04c9 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -37,12 +37,12 @@ def validate_storages_config storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") end - %w(failure_count_threshold failure_wait_time failure_reset_time storage_timeout).each do |setting| + %w(failure_count_threshold failure_reset_time storage_timeout).each do |setting| # Falling back to the defaults is fine! next if repository_storage[setting].nil? unless repository_storage[setting].to_f > 0 - storage_validation_error("#{setting}, for storage `#{name}` needs to be greater than 0") + storage_validation_error("`#{setting}` for storage `#{name}` needs to be greater than 0") end end end diff --git a/config/initializers/active_record_array_type_casting.rb b/config/initializers/active_record_array_type_casting.rb new file mode 100644 index 00000000000..d94d592add6 --- /dev/null +++ b/config/initializers/active_record_array_type_casting.rb @@ -0,0 +1,20 @@ +module ActiveRecord + class PredicateBuilder + class ArrayHandler + module TypeCasting + def call(attribute, value) + # This is necessary because by default ActiveRecord does not respect + # custom type definitions (like our `ShaAttribute`) when providing an + # array in `where`, like in `where(commit_sha: [sha1, sha2, sha3])`. + model = attribute.relation&.engine + type = model.user_provided_columns[attribute.name] if model + value = value.map { |value| type.type_cast_for_database(value) } if type + + super(attribute, value) + end + end + + prepend TypeCasting + end + end +end diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb index 9266ff0f615..150aaa2a8c2 100644 --- a/config/initializers/active_record_locking.rb +++ b/config/initializers/active_record_locking.rb @@ -18,7 +18,7 @@ module ActiveRecord lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i + previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend # This line is added as a patch previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 @@ -48,7 +48,7 @@ module ActiveRecord # If something went wrong, revert the version. rescue Exception - send(lock_col + '=', previous_lock_value) + send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend raise end end diff --git a/config/initializers/active_record_mysql_timestamp.rb b/config/initializers/active_record_mysql_timestamp.rb new file mode 100644 index 00000000000..af74c4ff6fb --- /dev/null +++ b/config/initializers/active_record_mysql_timestamp.rb @@ -0,0 +1,30 @@ +# Make sure that MySQL won't try to use CURRENT_TIMESTAMP when the timestamp +# column is NOT NULL. See https://gitlab.com/gitlab-org/gitlab-ce/issues/36405 +# And also: https://bugs.mysql.com/bug.php?id=75098 +# This patch was based on: +# https://github.com/rails/rails/blob/15ef55efb591e5379486ccf53dd3e13f416564f6/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb#L34-L36 + +if Gitlab::Database.mysql? + require 'active_record/connection_adapters/abstract/schema_creation' + + module MySQLTimestampFix + def add_column_options!(sql, options) + # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, + # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP + # column to contain NULL, explicitly declare it with the NULL attribute. + # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html + if sql.end_with?('timestamp') && !options[:primary_key] + if options[:null] != false + sql << ' NULL' + elsif options[:column].default.nil? + sql << ' DEFAULT 0' + end + end + + super + end + end + + ActiveRecord::ConnectionAdapters::AbstractAdapter::SchemaCreation + .prepend(MySQLTimestampFix) +end diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index 9ed96ddb0b4..943e01f1496 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,15 +1,15 @@ app = Rails.application if app.config.serve_static_files - # The `ActionDispatch::Static` middleware intercepts requests for static files - # by checking if they exist in the `/public` directory. + # The `ActionDispatch::Static` middleware intercepts requests for static files + # by checking if they exist in the `/public` directory. # We're replacing it with our `Gitlab::Middleware::Static` that does the same, # except ignoring `/uploads`, letting those go through to the GitLab Rails app. app.config.middleware.swap( - ActionDispatch::Static, - Gitlab::Middleware::Static, - app.paths["public"].first, + ActionDispatch::Static, + Gitlab::Middleware::Static, + app.paths["public"].first, app.config.static_cache_control ) diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb index fc4f02453d7..0c32528311e 100644 --- a/config/initializers/trusted_proxies.rb +++ b/config/initializers/trusted_proxies.rb @@ -2,7 +2,7 @@ # as the ActionDispatch::Request object. This is necessary for libraries # like rack_attack where they don't use ActionDispatch, and we want them # to block/throttle requests on private networks. -# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145 +# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145 module Rack class Request def trusted_proxy?(ip) diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index 5eb01d62924..0642a0b2fe9 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -1,3 +1,33 @@ +- group: Response metrics (NGINX Ingress) + priority: 10 + metrics: + - title: "Throughput" + y_label: "Requests / Sec" + required_metrics: + - nginx_upstream_requests_total + weight: 1 + queries: + - query_range: 'sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))' + label: Total + unit: req / sec + - title: "Latency" + y_label: "Latency (ms)" + required_metrics: + - nginx_upstream_response_msecs_avg + weight: 1 + queries: + - query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})' + label: Average + unit: ms + - title: "HTTP Error Rate" + y_label: "HTTP 500 Errors / Sec" + required_metrics: + - nginx_upstream_responses_total + weight: 1 + queries: + - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))' + label: HTTP Errors + unit: "errors / sec" - group: Response metrics (HA Proxy) priority: 10 metrics: @@ -68,18 +98,18 @@ - nginx_upstream_response_msecs_avg weight: 1 queries: - - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000' + - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})' label: Upstream unit: ms - title: "HTTP Error Rate" - y_label: "Error Rate (%)" + y_label: "HTTP 500 Errors / Sec" required_metrics: - nginx_responses_total weight: 1 queries: - - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) / sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m]))' + - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))' label: HTTP Errors - unit: "%" + unit: "errors / sec" - group: System metrics (Kubernetes) priority: 5 metrics: diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 2ba16035ece..9ffdebbcff1 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -3,6 +3,9 @@ resource :repository, only: [:create] do member do get ':ref/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, ref: /.+/ }, action: 'archive', as: 'archive' + + # deprecated since GitLab 9.5 + get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative' end end diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb index e9c9aa8b2f9..d7bca8310e4 100644 --- a/config/routes/uploads.rb +++ b/config/routes/uploads.rb @@ -5,12 +5,12 @@ scope path: :uploads do constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } # show uploads for models, snippets (notes) available for now - get 'system/:model/:id/:secret/:filename', + get '-/system/:model/:id/:secret/:filename', to: 'uploads#show', constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ } # show temporary uploads - get 'system/temp/:secret/:filename', + get '-/system/temp/:secret/:filename', to: 'uploads#show', constraints: { filename: /[^\/]+/ } diff --git a/config/webpack.config.js b/config/webpack.config.js index 8e1b80cd39f..6a347c2e660 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -223,6 +223,9 @@ var config = { names: ['main', 'locale', 'common', 'webpack_runtime'], }), + // enable scope hoisting + new webpack.optimize.ModuleConcatenationPlugin(), + // copy pre-compiled vendor libraries verbatim new CopyWebpackPlugin([ { diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index c304e0706dc..30244ee4431 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do project = Project.find_by_full_path('gitlab-org/gitlab-test') + next if project.empty_repo? # We don't have repository on CI + params = { source_branch: 'feature', target_branch: 'master', diff --git a/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb b/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb index ecdd1bd7e5e..f64dfa7675f 100644 --- a/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb +++ b/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/SaferBooleanColumn class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb index 915d3d78e40..46120652d8e 100644 --- a/db/migrate/20160817133006_add_koding_to_application_settings.rb +++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/SaferBooleanColumn class AddKodingToApplicationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb index b8f6a803a0a..fcdd79d3b02 100644 --- a/db/migrate/20161017125927_add_unique_index_to_labels.rb +++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb @@ -10,7 +10,7 @@ class AddUniqueIndexToLabels < ActiveRecord::Migration def up select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label| label_title = quote_string(label['title']) - duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] } + duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map { |label| label['id'] } label_id = duplicated_ids.first duplicated_ids.delete(label_id) diff --git a/db/migrate/20161020075830_default_request_access_projects.rb b/db/migrate/20161020075830_default_request_access_projects.rb index cb790291b24..a3a53350e8d 100644 --- a/db/migrate/20161020075830_default_request_access_projects.rb +++ b/db/migrate/20161020075830_default_request_access_projects.rb @@ -1,7 +1,7 @@ class DefaultRequestAccessProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers DOWNTIME = false - + def up change_column_default :projects, :request_access_enabled, false end diff --git a/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb index e644a174964..522437b92b4 100644 --- a/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb +++ b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/SaferBooleanColumn class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb index 1c59241d0fe..38f5781745b 100644 --- a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb +++ b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/SaferBooleanColumn class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb index 3677f978cc2..7f56ecf4c9e 100644 --- a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb +++ b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/SaferBooleanColumn class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170316163800_rename_system_namespaces.rb b/db/migrate/20170316163800_rename_system_namespaces.rb deleted file mode 100644 index 9e9fb5ac225..00000000000 --- a/db/migrate/20170316163800_rename_system_namespaces.rb +++ /dev/null @@ -1,231 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. -class RenameSystemNamespaces < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - include Gitlab::ShellAdapter - disable_ddl_transaction! - - class User < ActiveRecord::Base - self.table_name = 'users' - end - - class Namespace < ActiveRecord::Base - self.table_name = 'namespaces' - belongs_to :parent, class_name: 'RenameSystemNamespaces::Namespace' - has_one :route, as: :source - has_many :children, class_name: 'RenameSystemNamespaces::Namespace', foreign_key: :parent_id - belongs_to :owner, class_name: 'RenameSystemNamespaces::User' - - # Overridden to have the correct `source_type` for the `route` relation - def self.name - 'Namespace' - end - - def full_path - if route && route.path.present? - @full_path ||= route.path - else - update_route if persisted? - - build_full_path - end - end - - def build_full_path - if parent && path - parent.full_path + '/' + path - else - path - end - end - - def update_route - prepare_route - route.save - end - - def prepare_route - route || build_route(source: self) - route.path = build_full_path - route.name = build_full_name - @full_path = nil - @full_name = nil - end - - def build_full_name - if parent && name - parent.human_name + ' / ' + name - else - name - end - end - - def human_name - owner&.name - end - end - - class Route < ActiveRecord::Base - self.table_name = 'routes' - belongs_to :source, polymorphic: true - end - - class Project < ActiveRecord::Base - self.table_name = 'projects' - - def repository_storage_path - Gitlab.config.repositories.storages[repository_storage]['path'] - end - end - - DOWNTIME = false - - def up - return unless system_namespace - - old_path = system_namespace.path - old_full_path = system_namespace.full_path - # Only remove the last occurrence of the path name to get the parent namespace path - namespace_path = remove_last_occurrence(old_full_path, old_path) - new_path = rename_path(namespace_path, old_path) - new_full_path = join_namespace_path(namespace_path, new_path) - - Namespace.where(id: system_namespace).update_all(path: new_path) # skips callbacks & validations - - replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) - route_matches = [old_full_path, "#{old_full_path}/%"] - - update_column_in_batches(:routes, :path, replace_statement) do |table, query| - query.where(Route.arel_table[:path].matches_any(route_matches)) - end - - clear_cache_for_namespace(system_namespace) - - # tasks here are based on `Namespace#move_dir` - move_repositories(system_namespace, old_full_path, new_full_path) - move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage? - move_namespace_folders(pages_dir, old_full_path, new_full_path) - end - - def down - # nothing to do - end - - def remove_last_occurrence(string, pattern) - string.reverse.sub(pattern.reverse, "").reverse - end - - def move_namespace_folders(directory, old_relative_path, new_relative_path) - old_path = File.join(directory, old_relative_path) - return unless File.directory?(old_path) - - new_path = File.join(directory, new_relative_path) - FileUtils.mv(old_path, new_path) - 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 - gitlab_shell.add_namespace(repository_storage_path, old_full_path) - - unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) - say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}" - end - end - end - - def rename_path(namespace_path, path_was) - counter = 0 - path = "#{path_was}#{counter}" - - while route_exists?(join_namespace_path(namespace_path, path)) - counter += 1 - path = "#{path_was}#{counter}" - end - - path - end - - def route_exists?(full_path) - Route.where(Route.arel_table[:path].matches(full_path)).any? - end - - def join_namespace_path(namespace_path, path) - if namespace_path.present? - File.join(namespace_path, path) - else - path - end - end - - def system_namespace - @system_namespace ||= Namespace.where(parent_id: nil) - .where(arel_table[:path].matches(system_namespace_path)) - .first - end - - def system_namespace_path - "system" - end - - def clear_cache_for_namespace(namespace) - project_ids = projects_for_namespace(namespace).pluck(:id) - - update_column_in_batches(:projects, :description_html, nil) do |table, query| - query.where(table[:id].in(project_ids)) - end - - update_column_in_batches(:issues, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end - - update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| - query.where(table[:target_project_id].in(project_ids)) - end - - update_column_in_batches(:notes, :note_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end - - update_column_in_batches(:milestones, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end - end - - def projects_for_namespace(namespace) - namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) - namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids) - Project.unscoped.where(namespace_or_children) - end - - # This won't scale to huge trees, but it should do for a handful of namespaces - # called `system`. - def child_ids_for_parent(namespace, ids: []) - namespace.children.each do |child| - ids << child.id - child_ids_for_parent(child, ids: ids) if child.children.any? - end - ids - end - - def repo_paths_for_namespace(namespace) - projects_for_namespace(namespace).distinct - .select(:repository_storage).map(&:repository_storage_path) - end - - def uploads_dir - File.join(Rails.root, "public", "uploads") - end - - def pages_dir - Settings.pages.path - end - - def file_storage? - CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File - end - - def arel_table - Namespace.arel_table - end -end diff --git a/db/migrate/20170316163845_move_uploads_to_system_dir.rb b/db/migrate/20170316163845_move_uploads_to_system_dir.rb index 564ee10b5ab..cfcb909ddaf 100644 --- a/db/migrate/20170316163845_move_uploads_to_system_dir.rb +++ b/db/migrate/20170316163845_move_uploads_to_system_dir.rb @@ -54,6 +54,6 @@ class MoveUploadsToSystemDir < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "public", "uploads", "system") + File.join(base_directory, "public", "uploads", "-", "system") end end diff --git a/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb index 5e8b667b86d..d358020d182 100644 --- a/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb +++ b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/SaferBooleanColumn class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb b/db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb new file mode 100644 index 00000000000..f4f03bbabaf --- /dev/null +++ b/db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb @@ -0,0 +1,51 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PrepareEventsTableForPushEventsMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + # The order of these columns is deliberate and results in the following + # columns and sizes: + # + # * id (4 bytes) + # * project_id (4 bytes) + # * author_id (4 bytes) + # * target_id (4 bytes) + # * created_at (8 bytes) + # * updated_at (8 bytes) + # * action (2 bytes) + # * target_type (variable) + # + # Unfortunately we can't make the "id" column a bigint/bigserial as Rails 4 + # does not support this properly. + create_table :events_for_migration do |t| + t.references :project, + index: true, + foreign_key: { on_delete: :cascade } + + t.integer :author_id, index: true, null: false + t.integer :target_id + + t.timestamps_with_timezone null: false + + t.integer :action, null: false, limit: 2, index: true + t.string :target_type + + t.index %i[target_type target_id] + end + + # t.references doesn't like it when the column name doesn't make the table + # name so we have to add the foreign key separately. + add_concurrent_foreign_key(:events_for_migration, :users, column: :author_id) + end + + def down + drop_table :events_for_migration + end +end diff --git a/db/migrate/20170608152748_create_push_event_payloads_tables.rb b/db/migrate/20170608152748_create_push_event_payloads_tables.rb new file mode 100644 index 00000000000..6c55ad1f2f7 --- /dev/null +++ b/db/migrate/20170608152748_create_push_event_payloads_tables.rb @@ -0,0 +1,46 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreatePushEventPayloadsTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :push_event_payloads, id: false do |t| + t.bigint :commit_count, null: false + + t.integer :event_id, null: false + t.integer :action, null: false, limit: 2 + t.integer :ref_type, null: false, limit: 2 + + t.binary :commit_from + t.binary :commit_to + + t.text :ref + t.string :commit_title, limit: 70 + + t.index :event_id, unique: true + end + + # We're adding a foreign key to the _shadow_ table, and this is deliberate. + # By using the shadow table we don't have to recreate/revalidate this + # foreign key after swapping the "events_for_migration" and "events" tables. + # + # The "events_for_migration" table has a foreign key to "projects.id" + # ensuring that project removals also remove events from the shadow table + # (and thus also from this table). + add_concurrent_foreign_key( + :push_event_payloads, + :events_for_migration, + column: :event_id + ) + end + + def down + drop_table :push_event_payloads + end +end diff --git a/db/migrate/20170717074009_move_system_upload_folder.rb b/db/migrate/20170717074009_move_system_upload_folder.rb index cce31794115..d3caa53a7a4 100644 --- a/db/migrate/20170717074009_move_system_upload_folder.rb +++ b/db/migrate/20170717074009_move_system_upload_folder.rb @@ -15,6 +15,11 @@ class MoveSystemUploadFolder < ActiveRecord::Migration return end + if File.directory?(new_directory) + say "#{new_directory} already exists. No need to redo the move." + return + end + FileUtils.mkdir_p(File.join(base_directory, '-')) say "Moving #{old_directory} -> #{new_directory}" @@ -33,6 +38,11 @@ class MoveSystemUploadFolder < ActiveRecord::Migration return end + if !File.symlink?(old_directory) && File.directory?(old_directory) + say "#{old_directory} already exists and is not a symlink, no need to revert." + return + end + if File.symlink?(old_directory) say "Removing #{old_directory} -> #{new_directory} symlink" FileUtils.rm(old_directory) diff --git a/db/migrate/20170727123534_add_index_on_events_project_id_id.rb b/db/migrate/20170727123534_add_index_on_events_project_id_id.rb new file mode 100644 index 00000000000..1c4aaaf9dd6 --- /dev/null +++ b/db/migrate/20170727123534_add_index_on_events_project_id_id.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 AddIndexOnEventsProjectIdId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + COLUMNS = %i[project_id id].freeze + TABLES = %i[events events_for_migration].freeze + + disable_ddl_transaction! + + def up + TABLES.each do |table| + add_concurrent_index(table, COLUMNS) unless index_exists?(table, COLUMNS) + + # We remove the index _after_ adding the new one since MySQL doesn't let + # you remove an index when a foreign key exists for the same column. + if index_exists?(table, :project_id) + remove_concurrent_index(table, :project_id) + end + end + end + + def down + TABLES.each do |table| + unless index_exists?(table, :project_id) + add_concurrent_index(table, :project_id) + end + + unless index_exists?(table, COLUMNS) + remove_concurrent_index(table, COLUMNS) + end + end + end +end diff --git a/db/migrate/20170809133343_add_broadcast_messages_index.rb b/db/migrate/20170809133343_add_broadcast_messages_index.rb new file mode 100644 index 00000000000..4ab2ddb059d --- /dev/null +++ b/db/migrate/20170809133343_add_broadcast_messages_index.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddBroadcastMessagesIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + COLUMNS = %i[starts_at ends_at id].freeze + + def up + add_concurrent_index :broadcast_messages, COLUMNS + end + + def down + remove_concurrent_index :broadcast_messages, COLUMNS + end +end diff --git a/db/migrate/20170809134534_add_broadcast_message_not_null_constraints.rb b/db/migrate/20170809134534_add_broadcast_message_not_null_constraints.rb new file mode 100644 index 00000000000..5551fb51a6e --- /dev/null +++ b/db/migrate/20170809134534_add_broadcast_message_not_null_constraints.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + COLUMNS = %i[starts_at ends_at created_at updated_at message_html] + + class BroadcastMessage < ActiveRecord::Base + self.table_name = 'broadcast_messages' + end + + def up + COLUMNS.each do |column| + BroadcastMessage.where(column => nil).delete_all + + change_column_null :broadcast_messages, column, false + end + end + + def down + COLUMNS.each do |column| + change_column_null :broadcast_messages, column, true + end + end +end diff --git a/db/migrate/20170809142252_cleanup_appearances_schema.rb b/db/migrate/20170809142252_cleanup_appearances_schema.rb new file mode 100644 index 00000000000..90d12925ba2 --- /dev/null +++ b/db/migrate/20170809142252_cleanup_appearances_schema.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanupAppearancesSchema < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + NOT_NULL_COLUMNS = %i[title description description_html created_at updated_at] + + TIME_COLUMNS = %i[created_at updated_at] + + def up + NOT_NULL_COLUMNS.each do |column| + change_column_null :appearances, column, false + end + + TIME_COLUMNS.each do |column| + change_column :appearances, column, :datetime_with_timezone + end + end + + def down + NOT_NULL_COLUMNS.each do |column| + change_column_null :appearances, column, true + end + + TIME_COLUMNS.each do |column| + change_column :appearances, column, :datetime # rubocop: disable Migration/Datetime + end + end +end diff --git a/db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb b/db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb new file mode 100644 index 00000000000..4baba1ade6d --- /dev/null +++ b/db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb @@ -0,0 +1,14 @@ +class AddProjectExportEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:application_settings, :project_export_enabled, :boolean, default: true) + end + + def down + remove_column(:application_settings, :project_export_enabled) + end +end diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb index ca2912f8dce..92e33848bf0 100644 --- a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb +++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb @@ -48,7 +48,7 @@ class UpdateUploadPathsToSystem < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "system") + File.join(base_directory, "-", "system") end def arel_table diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb index fc3a4acc0bb..f2ce25d4524 100644 --- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb +++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb @@ -47,6 +47,6 @@ class CleanUploadSymlinks < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "public", "uploads", "system") + File.join(base_directory, "public", "uploads", "-", "system") end end diff --git a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb index c1e64f20109..5238a2ba1b7 100644 --- a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb +++ b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb @@ -30,7 +30,7 @@ class CleanupNamespacelessPendingDeleteProjects < ActiveRecord::Migration private def pending_delete_batch - connection.exec_query(find_batch).map{ |row| row['id'].to_i } + connection.exec_query(find_batch).map { |row| row['id'].to_i } end BATCH_SIZE = 5000 diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb index 705e11ed47d..3a4d6c4916b 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -21,7 +21,7 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration private def up_mysql - # This is a trick to overcome MySQL limitation: + # 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 diff --git a/db/post_migrate/20170523083112_migrate_old_artifacts.rb b/db/post_migrate/20170523083112_migrate_old_artifacts.rb index f2690bd0017..3a77b9751d3 100644 --- a/db/post_migrate/20170523083112_migrate_old_artifacts.rb +++ b/db/post_migrate/20170523083112_migrate_old_artifacts.rb @@ -7,7 +7,7 @@ class MigrateOldArtifacts < ActiveRecord::Migration # This uses special heuristic to find potential candidates for data migration # Read more about this here: https://gitlab.com/gitlab-org/gitlab-ce/issues/32036#note_30422345 - + def up builds_with_artifacts.find_each do |build| build.migrate_artifacts! @@ -51,14 +51,14 @@ class MigrateOldArtifacts < ActiveRecord::Migration private def source_artifacts_path - @source_artifacts_path ||= + @source_artifacts_path ||= File.join(Gitlab.config.artifacts.path, created_at.utc.strftime('%Y_%m'), ci_id.to_s, id.to_s) end def target_artifacts_path - @target_artifacts_path ||= + @target_artifacts_path ||= File.join(Gitlab.config.artifacts.path, created_at.utc.strftime('%Y_%m'), project_id.to_s, id.to_s) diff --git a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb index 561de59ec69..07935ab8a52 100644 --- a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb +++ b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb @@ -52,6 +52,6 @@ class MoveAppearanceToSystemDir < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "public", "uploads", "system") + File.join(base_directory, "public", "uploads", "-", "system") end end diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb index 33043364bde..2b79a87ccd8 100644 --- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb +++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb @@ -10,7 +10,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration return unless file_storage? @source_relative_location = File.join('/uploads', 'personal_snippet') - @destination_relative_location = File.join('/uploads', 'system', 'personal_snippet') + @destination_relative_location = File.join('/uploads', '-', 'system', 'personal_snippet') move_personal_snippet_files end @@ -18,7 +18,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration def down return unless file_storage? - @source_relative_location = File.join('/uploads', 'system', 'personal_snippet') + @source_relative_location = File.join('/uploads', '-', 'system', 'personal_snippet') @destination_relative_location = File.join('/uploads', 'personal_snippet') move_personal_snippet_files diff --git a/db/post_migrate/20170627101016_schedule_event_migrations.rb b/db/post_migrate/20170627101016_schedule_event_migrations.rb new file mode 100644 index 00000000000..1f34375ff0d --- /dev/null +++ b/db/post_migrate/20170627101016_schedule_event_migrations.rb @@ -0,0 +1,40 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ScheduleEventMigrations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BUFFER_SIZE = 1000 + + disable_ddl_transaction! + + class Event < ActiveRecord::Base + include EachBatch + + self.table_name = 'events' + end + + def up + jobs = [] + + Event.each_batch(of: 1000) do |relation| + min, max = relation.pluck('MIN(id), MAX(id)').first + + if jobs.length == BUFFER_SIZE + # We push multiple jobs at a time to reduce the time spent in + # Sidekiq/Redis operations. We're using this buffer based approach so we + # don't need to run additional queries for every range. + BackgroundMigrationWorker.perform_bulk(jobs) + jobs.clear + end + + jobs << ['MigrateEventsToPushEventPayloads', [min, max]] + end + + BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty? + end + + def down + end +end diff --git a/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb b/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb new file mode 100644 index 00000000000..e3d2446b897 --- /dev/null +++ b/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MovePersonalSnippetFilesIntoCorrectFolder < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + NEW_DIRECTORY = File.join('/uploads', '-', 'system', 'personal_snippet') + OLD_DIRECTORY = File.join('/uploads', 'system', 'personal_snippet') + + def up + return unless file_storage? + + BackgroundMigrationWorker.perform_async('MovePersonalSnippetFiles', + [OLD_DIRECTORY, NEW_DIRECTORY]) + end + + def down + return unless file_storage? + + BackgroundMigrationWorker.perform_async('MovePersonalSnippetFiles', + [NEW_DIRECTORY, OLD_DIRECTORY]) + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end +end diff --git a/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb b/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb new file mode 100644 index 00000000000..6132b553177 --- /dev/null +++ b/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb @@ -0,0 +1,26 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDuplicateMrEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + class Event < ActiveRecord::Base + self.table_name = 'events' + end + + def up + base_condition = "action = 1 AND target_type = 'MergeRequest' AND created_at > '2017-08-13'" + Event.select('target_id, count(*)') + .where(base_condition) + .group('target_id').having('count(*) > 1').each do |event| + duplicates = Event.where("#{base_condition} AND target_id = #{event.target_id}").pluck(:id) + duplicates.shift + + Event.where(id: duplicates).delete_all + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index ed3cf70bcdd..2ea6ae29dc7 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: 20170807160457) do +ActiveRecord::Schema.define(version: 20170815060945) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -28,13 +28,13 @@ ActiveRecord::Schema.define(version: 20170807160457) do end create_table "appearances", force: :cascade do |t| - t.string "title" - t.text "description" + t.string "title", null: false + t.text "description", null: false t.string "header_logo" t.string "logo" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "description_html" + t.text "description_html", null: false t.integer "cached_markdown_version" end @@ -127,6 +127,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do t.string "help_page_support_url" t.integer "performance_bar_allowed_group_id" t.boolean "password_authentication_enabled" + t.boolean "project_export_enabled", default: true, null: false end create_table "audit_events", force: :cascade do |t| @@ -163,16 +164,18 @@ ActiveRecord::Schema.define(version: 20170807160457) do create_table "broadcast_messages", force: :cascade do |t| t.text "message", null: false - t.datetime "starts_at" - t.datetime "ends_at" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "starts_at", null: false + t.datetime "ends_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "color" t.string "font" - t.text "message_html" + t.text "message_html", null: false t.integer "cached_markdown_version" end + add_index "broadcast_messages", ["starts_at", "ends_at", "id"], name: "index_broadcast_messages_on_starts_at_and_ends_at_and_id", using: :btree + create_table "chat_names", force: :cascade do |t| t.integer "user_id", null: false t.integer "service_id", null: false @@ -530,10 +533,25 @@ ActiveRecord::Schema.define(version: 20170807160457) do add_index "events", ["action"], name: "index_events_on_action", using: :btree add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree - add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree + add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree + create_table "events_for_migration", force: :cascade do |t| + t.integer "project_id" + t.integer "author_id", null: false + t.integer "target_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "action", limit: 2, null: false + t.string "target_type" + end + + add_index "events_for_migration", ["action"], name: "index_events_for_migration_on_action", using: :btree + add_index "events_for_migration", ["author_id"], name: "index_events_for_migration_on_author_id", using: :btree + add_index "events_for_migration", ["project_id", "id"], name: "index_events_for_migration_on_project_id_and_id", using: :btree + add_index "events_for_migration", ["target_type", "target_id"], name: "index_events_for_migration_on_target_type_and_target_id", using: :btree + create_table "feature_gates", force: :cascade do |t| t.string "feature_key", null: false t.string "key", null: false @@ -1254,6 +1272,19 @@ ActiveRecord::Schema.define(version: 20170807160457) do add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree + create_table "push_event_payloads", id: false, force: :cascade do |t| + t.integer "commit_count", limit: 8, null: false + t.integer "event_id", null: false + t.integer "action", limit: 2, null: false + t.integer "ref_type", limit: 2, null: false + t.binary "commit_from" + t.binary "commit_to" + t.text "ref" + t.string "commit_title", limit: 70 + end + + add_index "push_event_payloads", ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true, using: :btree + create_table "redirect_routes", force: :cascade do |t| t.integer "source_id", null: false t.string "source_type", null: false @@ -1654,6 +1685,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade + add_foreign_key "events_for_migration", "projects", on_delete: :cascade + add_foreign_key "events_for_migration", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "gpg_keys", "users", on_delete: :cascade add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify @@ -1696,6 +1729,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do add_foreign_key "protected_tag_create_access_levels", "protected_tags" add_foreign_key "protected_tag_create_access_levels", "users" add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade + add_foreign_key "push_event_payloads", "events_for_migration", column: "event_id", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 8bb8e147cd1..547541c4876 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,9 +1,13 @@ +--- +toc: false +--- + # GitLab Documentation Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured platform for software development! -We offer four different products for you and your company: +GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans: - **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/), self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. @@ -26,6 +30,10 @@ Shortcuts to GitLab's most visited docs: | [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) | | [Using Docker images](ci/docker/using_docker_images.md) | [GitLab Pages](user/project/pages/index.md) | +- [User documentation](user/index.md) +- [Administrator documentation](#administrator-documentation) +- [Technical Articles](articles/index.md) + ## Getting started with GitLab - [GitLab Basics](gitlab-basics/README.md): Start working on your command line and on GitLab. @@ -36,7 +44,6 @@ Shortcuts to GitLab's most visited docs: ### User account -- [User documentation](user/index.md): Learn how to use GitLab and explore its features - [User account](user/profile/index.md): Manage your account - [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects. - [Profile settings](user/profile/index.md#profile-settings): Manage your profile settings, two factor authentication and more. diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 90a2e9298bf..e09ccaba08c 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -42,6 +42,10 @@ GitLab does not recommend using EFS with GitLab. 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. +- To keep "Burst Credits" available, it may be necessary to provision more space + with 'dummy data'. However, this may get expensive. +- Another option to maintain "Burst Credits" is to use FS Cache on the server so + that AWS doesn't always have to go into EFS to access files. - 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. diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 4b8d5c5cc87..76e071dc673 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -9,6 +9,33 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. +## `production_json.log` + +This file lives in `/var/log/gitlab/gitlab-rails/production_json.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/production_json.log` for +installations from source. (When Gitlab is running in an environment +other than production, the corresponding logfile is shown here.) + +It contains a structured log for Rails controller requests received from +GitLab, thanks to [Lograge](https://github.com/roidrage/lograge/). Note that +requests from the API [are not yet logged to this +file](https://gitlab.com/gitlab-org/gitlab-ce/issues/36189). + +Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example: + +```json +{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":{"namespace_id":"gitlab","project_id":"gitlab-ce","id":"1234"},"remote_ip":"18.245.0.1","user_id":1,"username":"admin"} +``` + +In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data: + +1. `duration`: the total time taken to retrieve the request +2. `view`: total time taken inside the Rails views +3. `db`: total time to retrieve data from the database + +In addition, the log contains the IP address from which the request originated +(`remote_ip`) as well as the user's ID (`user_id`), and username (`username`). + ## `production.log` This file lives in `/var/log/gitlab/gitlab-rails/production.log` for diff --git a/doc/api/events.md b/doc/api/events.md index 3d5170f3f1e..129af0afa35 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -79,7 +79,6 @@ Example response: "target_id":160, "target_type":"Issue", "author_id":25, - "data":null, "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.", "created_at":"2017-02-09T10:43:19.667Z", "author":{ @@ -99,7 +98,6 @@ Example response: "target_id":159, "target_type":"Issue", "author_id":21, - "data":null, "target_title":"Nostrum enim non et sed optio illo deleniti non.", "created_at":"2017-02-09T10:43:19.426Z", "author":{ @@ -151,7 +149,6 @@ Example response: "target_id": 830, "target_type": "Issue", "author_id": 1, - "data": null, "target_title": "Public project search field", "author": { "name": "Dmitriy Zaporozhets", @@ -166,7 +163,7 @@ Example response: { "title": null, "project_id": 15, - "action_name": "opened", + "action_name": "pushed", "target_id": null, "target_type": null, "author_id": 1, @@ -179,31 +176,14 @@ Example response: "web_url": "http://localhost:3000/root" }, "author_username": "john", - "data": { - "before": "50d4420237a9de7be1304607147aec22e4a14af7", - "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "ref": "refs/heads/master", - "user_id": 1, - "user_name": "Dmitriy Zaporozhets", - "repository": { - "name": "gitlabhq", - "url": "git@dev.gitlab.org:gitlab/gitlabhq.git", - "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.", - "homepage": "https://dev.gitlab.org/gitlab/gitlabhq" - }, - "commits": [ - { - "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "message": "Add simple search to projects in public area", - "timestamp": "2013-05-13T18:18:08+00:00", - "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", - "author": { - "name": "Dmitriy Zaporozhets", - "email": "dmitriy.zaporozhets@gmail.com" - } - } - ], - "total_commits_count": 1 + "push_data": { + "commit_count": 1, + "action": "pushed", + "ref_type": "branch", + "commit_from": "50d4420237a9de7be1304607147aec22e4a14af7", + "commit_to": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "ref": "master", + "commit_title": "Add simple search to projects in public area" }, "target_title": null }, @@ -214,7 +194,6 @@ Example response: "target_id": 840, "target_type": "Issue", "author_id": 1, - "data": null, "target_title": "Finish & merge Code search PR", "author": { "name": "Dmitriy Zaporozhets", @@ -233,7 +212,6 @@ Example response: "target_id": 1312, "target_type": "Note", "author_id": 1, - "data": null, "target_title": null, "created_at": "2015-12-04T10:33:58.089Z", "note": { @@ -305,7 +283,6 @@ Example response: "target_iid":160, "target_type":"Issue", "author_id":25, - "data":null, "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.", "created_at":"2017-02-09T10:43:19.667Z", "author":{ @@ -326,7 +303,6 @@ Example response: "target_iid":159, "target_type":"Issue", "author_id":21, - "data":null, "target_title":"Nostrum enim non et sed optio illo deleniti non.", "created_at":"2017-02-09T10:43:19.426Z", "author":{ diff --git a/doc/articles/artifactory_and_gitlab/index.md b/doc/articles/artifactory_and_gitlab/index.md new file mode 100644 index 00000000000..c64851bad2b --- /dev/null +++ b/doc/articles/artifactory_and_gitlab/index.md @@ -0,0 +1,278 @@ +# How to deploy Maven projects to Artifactory with GitLab CI/CD + +> **Article [Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) || +> **Publication date:** 2017-08-15 + +## Introduction + +In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) +to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/), and then use it from another Maven application as a dependency. + +You'll create two different projects: + +- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep) +- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app) + +We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/). +We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. + +## Create the simple Maven dependency + +First of all, you need an application to work with: in this specific case we will +use a simple one, but it could be any Maven application. This will be the +dependency you want to package and deploy to Artifactory, in order to be +available to other projects. + +### Prepare the dependency application + +For this article you'll use a Maven app that can be cloned from our example +project: + +1. Log in to your GitLab account +1. Create a new project by selecting **Import project from ➔ Repo by URL** +1. Add the following URL: + + ``` + https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git + ``` +1. Click **Create project** + +This application is nothing more than a basic class with a stub for a JUnit based test suite. +It exposes a method called `hello` that accepts a string as input, and prints a hello message on the screen. + +The project structure is really simple, and you should consider these two resources: + +- `pom.xml`: project object model (POM) configuration file +- `src/main/java/com/example/dep/Dep.java`: source of our application + +### Configure the Artifactory deployment + +The application is ready to use, but you need some additional steps to deploy it to Artifactory: + +1. Log in to Artifactory with your user's credentials. +1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel. +1. Copy to clipboard the configuration snippet under the **Deploy** paragraph. +1. Change the `url` value in order to have it configurable via secret variables. +1. Copy the snippet in the `pom.xml` file for your project, just after the + `dependencies` section. The snippet should look like this: + + ```xml + <distributionManagement> + <repository> + <id>central</id> + <name>83d43b5afeb5-releases</name> + <url>${env.MAVEN_REPO_URL}/libs-release-local</url> + </repository> + </distributionManagement> + ``` + +Another step you need to do before you can deploy the dependency to Artifactory +is to configure the authentication data. It is a simple task, but Maven requires +it to stay in a file called `settings.xml` that has to be in the `.m2` subdirectory +in the user's homedir. + +Since you want to use GitLab Runner to automatically deploy the application, you +should create the file in the project's home directory and set a command line +parameter in `.gitlab-ci.yml` to use the custom location instead of the default one: + +1. Create a folder called `.m2` in the root of your repository +1. Create a file called `settings.xml` in the `.m2` folder +1. Copy the following content into a `settings.xml` file: + + ```xml + <settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" + xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <servers> + <server> + <id>central</id> + <username>${env.MAVEN_REPO_USER}</username> + <password>${env.MAVEN_REPO_PASS}</password> + </server> + </servers> + </settings> + ``` + + Username and password will be replaced by the correct values using secret variables. + +### Configure GitLab CI/CD for `simple-maven-dep` + +Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and deploy the dependency! + +GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs +that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/). + +First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page +and add the following secret variables (replace them with your current values, of course): + +- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) +- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) +- **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password) + +Now it's time to define jobs in `.gitlab-ci.yml` and push it to the repo: + +```yaml +image: maven:latest + +variables: + MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" + MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" + +cache: + paths: + - .m2/repository/ + - target/ + +build: + stage: build + script: + - mvn $MAVEN_CLI_OPTS compile + +test: + stage: test + script: + - mvn $MAVEN_CLI_OPTS test + +deploy: + stage: deploy + script: + - mvn $MAVEN_CLI_OPTS deploy + only: + - master +``` + +GitLab Runner will use the latest [Maven Docker image](https://hub.docker.com/_/maven/), which already contains all the tools and the dependencies you need to manage the project, +in order to run the jobs. + +Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home when searching for configuration and dependencies. + +Caching the `.m2/repository folder` (where all the Maven files are stored), and the `target` folder (where our application will be created), is useful for speeding up the process +by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary. + +Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application. + +Deploy to Artifactory is done as defined by the secret variables we have just set up. +The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. + +Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. +If the deployment has been successful, the deploy job log will output: + +``` +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 1.983 s +``` + +>**Note**: +the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log the first time you run it. + +Yay! You did it! Checking in Artifactory will confirm that you have a new artifact available in the `libs-release-local` repo. + +## Create the main Maven application + +Now that you have the dependency available on Artifactory, it's time to use it! +Let's see how we can have it as a dependency to our main application. + +### Prepare the main application + +We'll use again a Maven app that can be cloned from our example project: + +1. Create a new project by selecting **Import project from ➔ Repo by URL** +1. Add the following URL: + + ``` + https://gitlab.com/gitlab-examples/maven/simple-maven-app.git + ``` +1. Click **Create project** + +This one is a simple app as well. If you look at the `src/main/java/com/example/app/App.java` +file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. + +Since Maven doesn't know how to resolve the dependency, you need to modify the configuration: + +1. Go back to Artifactory +1. Browse the `libs-release-local` repository +1. Select the `simple-maven-dep-1.0.jar` file +1. Find the configuration snippet from the **Dependency Declaration** section of the main panel +1. Copy the snippet in the `dependencies` section of the `pom.xml` file. + The snippet should look like this: + + ```xml + <dependency> + <groupId>com.example.dep</groupId> + <artifactId>simple-maven-dep</artifactId> + <version>1.0</version> + </dependency> + ``` + +### Configure the Artifactory repository location + +At this point you defined the dependency for the application, but you still miss where you can find the required files. +You need to create a `.m2/settings.xml` file as you did for the dependency project, and let Maven know the location using environment variables. + +Here is how you can get the content of the file directly from Artifactory: + +1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel +1. Click on **Generate Maven Settings** +1. Click on **Generate Settings** +1. Copy to clipboard the configuration file +1. Save the file as `.m2/settings.xml` in your repo + +Now you are ready to use the Artifactory repository to resolve dependencies and use `simple-maven-dep` in your main application! + +### Configure GitLab CI/CD for `simple-maven-app` + +You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`. + +You want to leverage [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and run your awesome application, +and see if you can get the greeting as expected! + +All you need to do is to add the following `.gitlab-ci.yml` to the repo: + +```yaml +image: maven:latest + +stages: + - build + - test + - run + +variables: + MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" + MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" + +cache: + paths: + - .m2/repository/ + - target/ + +build: + stage: build + script: + - mvn $MAVEN_CLI_OPTS compile + +test: + stage: test + script: + - mvn $MAVEN_CLI_OPTS test + +run: + stage: run + script: + - mvn $MAVEN_CLI_OPTS package + - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App" +``` + +It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job. +Probably something that you don't want to use in real projects, but here it is useful to see the application executed automatically. + +And that's it! In the `run` job output log you will find a friendly hello to GitLab! + +## Conclusion + +In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume artifacts. + +A similar approach could be used to interact with any other Maven compatible Binary Repository Manager. +Obviously, you can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow. diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md index 130e8f542b4..25a24bc1d32 100644 --- a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md +++ b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md @@ -3,7 +3,7 @@ > **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** admin guide || > **Level:** intermediary || > **Author:** [Chris Wilson](https://gitlab.com/MrChrisW) || -> **Publication date:** 2017/05/03 +> **Publication date:** 2017-05-03 ## Introduction diff --git a/doc/articles/how_to_install_git/index.md b/doc/articles/how_to_install_git/index.md index 66d866b2d09..37b60501ce2 100644 --- a/doc/articles/how_to_install_git/index.md +++ b/doc/articles/how_to_install_git/index.md @@ -3,7 +3,7 @@ > **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** user guide || > **Level:** beginner || > **Author:** [Sean Packham](https://gitlab.com/SeanPackham) || -> **Publication date:** 2017/05/15 +> **Publication date:** 2017-05-15 To begin contributing to GitLab projects you will need to install the Git client on your computer. diff --git a/doc/articles/index.md b/doc/articles/index.md index 558c624fe39..3039faca411 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -26,6 +26,7 @@ Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/READM | Article title | Category | Publishing date | | :------------ | :------: | --------------: | +| [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md) | Tutorial | 2017-08-15 | | [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017/07/13 | | [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) | Concepts | 2017/07/11 | | [Continuous Integration: From Jenkins to GitLab Using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/) | Concepts | 2017/07/27 | diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md index 7f76e577efa..c0bbcfe2a8a 100644 --- a/doc/articles/openshift_and_gitlab/index.md +++ b/doc/articles/openshift_and_gitlab/index.md @@ -3,7 +3,7 @@ > **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || > **Level:** intermediary || > **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) || -> **Publication date:** 2016/06/28 +> **Publication date:** 2016-06-28 ## Introduction diff --git a/doc/ci/README.md b/doc/ci/README.md index 10ea9467942..c722d895f42 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -111,7 +111,8 @@ Here is an collection of tutorials and guides on setting up your CI pipeline. - [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** +- **Articles** + - [How to deploy Maven projects to Artifactory with GitLab CI/CD](../articles/artifactory_and_gitlab/index.md) - [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/) - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) diff --git a/doc/ci/autodeploy/img/auto_monitoring.png b/doc/ci/autodeploy/img/auto_monitoring.png Binary files differnew file mode 100644 index 00000000000..5661b50841b --- /dev/null +++ b/doc/ci/autodeploy/img/auto_monitoring.png diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index 9fa2b2c4969..a714689ebd5 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -69,3 +69,28 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE [review-app]: ../review_apps/index.md [container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html [postgresql]: https://www.postgresql.org/ + +## Auto Monitoring + +> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438). + +Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for: + +* Response Metrics: latency, throughput, error rate +* System Metrics: CPU utilization, memory utilization + +Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md). + +To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments). + +![Auto Metrics](img/auto_monitoring.png) + +### Configuring Auto Monitoring + +If GitLab has been deployed using the [omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no configuration is required. + +If you have installed GitLab using a different method: + +1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster +1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml). +1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 6a7f694d705..28b27921f8b 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -607,10 +607,9 @@ exist, you should see something like: - With GitLab 9.2, all deployments to an environment are shown directly on the monitoring dashboard -If you have enabled Prometheus for collecting metrics, you can monitor the performance behavior of your app -through the environments. +If you have enabled [Prometheus for monitoring system and response metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus.html), you can monitor the performance behavior of your app running in each environment. -Once configured, GitLab will attempt to retrieve performance metrics for any +Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) 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. diff --git a/doc/development/README.md b/doc/development/README.md index 58993c52dcd..dd150421b65 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -46,6 +46,7 @@ ## Databases +- [Merge Request Checklist](database_merge_request_checklist.md) - [What requires downtime?](what_requires_downtime.md) - [Adding database indexes](adding_database_indexes.md) - [Post Deployment Migrations](post_deployment_migrations.md) @@ -56,6 +57,9 @@ - [Background Migrations](background_migrations.md) - [Storing SHA1 Hashes As Binary](sha1_as_binary.md) - [Iterating Tables In Batches](iterating_tables_in_batches.md) +- [Ordering Table Columns](ordering_table_columns.md) +- [Verifying Database Capabilities](verifying_database_capabilities.md) +- [Hash Indexes](hash_indexes.md) ## i18n diff --git a/doc/development/changelog.md b/doc/development/changelog.md index ce39a379a0e..f869938fe11 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -15,11 +15,14 @@ following format: title: "Going through change[log]s" merge_request: 1972 author: Ozzy Osbourne +type: added ``` The `merge_request` value is a reference to a merge request that adds this entry, and the `author` key is used to give attribution to community contributors. **Both are optional**. +The `type` field maps the category of the change, +valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**. Community contributors and core team members are encouraged to add their name to the `author` field. GitLab team members **should not**. @@ -94,6 +97,19 @@ Its simplest usage is to provide the value for `title`: $ bin/changelog 'Hey DZ, I added a feature to GitLab!' ``` +At this point the script would ask you to select the category of the change (mapped to the `type` field in the entry): + +```text +>> Please specify the category of your change: +1. New feature +2. Bug fix +3. Feature change +4. New deprecation +5. Feature removal +6. Security fix +7. Other +``` + The entry filename is based on the name of the current Git branch. If you run the command above on a branch called `feature/hey-dz`, it will generate a `changelogs/unreleased/feature-hey-dz.yml` file. @@ -106,26 +122,29 @@ create changelogs/unreleased/my-feature.yml title: Hey DZ, I added a feature to GitLab! merge_request: author: +type: ``` If you're working on the GitLab EE repository, the entry will be added to `changelogs/unreleased-ee/` instead. #### Arguments -| Argument | Shorthand | Purpose | -| ----------------- | --------- | --------------------------------------------- | -| [`--amend`] | | Amend the previous commit | -| [`--force`] | `-f` | Overwrite an existing entry | -| [`--merge-request`] | `-m` | Set merge request ID | -| [`--dry-run`] | `-n` | Don't actually write anything, just print | -| [`--git-username`] | `-u` | Use Git user.name configuration as the author | -| [`--help`] | `-h` | Print help message | +| Argument | Shorthand | Purpose | +| ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- | +| [`--amend`] | | Amend the previous commit | +| [`--force`] | `-f` | Overwrite an existing entry | +| [`--merge-request`] | `-m` | Set merge request ID | +| [`--dry-run`] | `-n` | Don't actually write anything, just print | +| [`--git-username`] | `-u` | Use Git user.name configuration as the author | +| [`--type`] | `-t` | The category of the change, valid options are: added, fixed, changed, deprecated, removed, security, other | +| [`--help`] | `-h` | Print help message | [`--amend`]: #-amend [`--force`]: #-force-or-f [`--merge-request`]: #-merge-request-or-m [`--dry-run`]: #-dry-run-or-n [`--git-username`]: #-git-username-or-u +[`--type`]: #-type-or-t [`--help`]: #-help ##### `--amend` @@ -147,6 +166,7 @@ create changelogs/unreleased/feature-hey-dz.yml title: Added an awesome new feature to GitLab merge_request: author: +type: ``` ##### `--force` or `-f` @@ -164,6 +184,7 @@ create changelogs/unreleased/feature-hey-dz.yml title: Hey DZ, I added a feature to GitLab! merge_request: 1983 author: +type: ``` ##### `--merge-request` or `-m` @@ -178,6 +199,7 @@ create changelogs/unreleased/feature-hey-dz.yml title: Hey DZ, I added a feature to GitLab! merge_request: 1983 author: +type: ``` ##### `--dry-run` or `-n` @@ -192,6 +214,7 @@ create changelogs/unreleased/feature-hey-dz.yml title: Added an awesome new feature to GitLab merge_request: author: +type: $ ls changelogs/unreleased/ ``` @@ -211,6 +234,21 @@ create changelogs/unreleased/feature-hey-dz.yml title: Hey DZ, I added a feature to GitLab! merge_request: author: Jane Doe +type: +``` + +##### `--type` or `-t` + +Use the **`--type`** or **`-t`** argument to provide the `type` value: + +```text +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -t added +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: +author: +type: added ``` ### History and Reasoning diff --git a/doc/development/database_merge_request_checklist.md b/doc/development/database_merge_request_checklist.md new file mode 100644 index 00000000000..75c395b61ef --- /dev/null +++ b/doc/development/database_merge_request_checklist.md @@ -0,0 +1,15 @@ +# Merge Request Checklist + +When creating a merge request that performs database related changes (schema +changes, adjusting queries to optimise performance, etc) you should use the +merge request template called "Database Changes". This template contains a +checklist of steps to follow to make sure the changes are up to snuff. + +To use the checklist, create a new merge request and click on the "Choose a +template" dropdown, then click "Database Changes". + +An example of this checklist can be found at +https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12463. + +The source code of the checklist can be found in at +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Database%20Changes.md diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 6ade3231fac..9c72fda0229 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -511,7 +511,24 @@ A forEach will cause side effects, it will be mutating the array being iterated. $('span').tooltip('fixTitle'); ``` +### The Javascript/Vue Accord +The goal of this accord is to make sure we are all on the same page. +1. When writing Vue, you may not use jQuery in your application. +1.1 If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery. +1.2 You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html). +1.3 If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners. +1.4 We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit). + +1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application. + +1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with it's priority to be determined by maintainers. + +1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests. + +1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch). + +1. Once you have chosen a centralized state management solution you must use it for your entire application. i.e. Don't mix and match your state management solutions. ## SCSS - [SCSS](style_guide_scss.md) diff --git a/doc/development/hash_indexes.md b/doc/development/hash_indexes.md new file mode 100644 index 00000000000..e6c1b3590b1 --- /dev/null +++ b/doc/development/hash_indexes.md @@ -0,0 +1,20 @@ +# Hash Indexes + +Both PostgreSQL and MySQL support hash indexes besides the regular btree +indexes. Hash indexes however are to be avoided at all costs. While they may +_sometimes_ provide better performance the cost of rehashing can be very high. +More importantly: at least until PostgreSQL 10.0 hash indexes are not +WAL-logged, meaning they are not replicated to any replicas. From the PostgreSQL +documentation: + +> Hash index operations are not presently WAL-logged, so hash indexes might need +> to be rebuilt with REINDEX after a database crash if there were unwritten +> changes. Also, changes to hash indexes are not replicated over streaming or +> file-based replication after the initial base backup, so they give wrong +> answers to queries that subsequently use them. For these reasons, hash index +> use is presently discouraged. + +RuboCop is configured to register an offence when it detects the use of a hash +index. + +Instead of using hash indexes you should use regular btree indexes. diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md new file mode 100644 index 00000000000..249e70c7b0e --- /dev/null +++ b/doc/development/ordering_table_columns.md @@ -0,0 +1,127 @@ +# Ordering Table Columns + +Similar to C structures the space of a table is influenced by the order of +columns. This is because the size of columns is aligned depending on the type of +the column. Take the following column order for example: + +* id (integer, 4 bytes) +* name (text, variable) +* user_id (integer, 4 bytes) + +Integers are aligned to the word size. This means that on a 64 bit platform the +actual size of each column would be: 8 bytes, variable, 8 bytes. This means that +each row will require at least 16 bytes for the two integers, and a variable +amount for the text field. If a table has a few rows this is not an issue, but +once you start storing millions of rows you can save space by using a different +order. For the above example a more ideal column order would be the following: + +* id (integer, 4 bytes) +* user_id (integer, 4 bytes) +* name (text, variable) + +In this setup the `id` and `user_id` columns can be packed together, which means +we only need 8 bytes to store _both_ of them. This in turn each row will require +8 bytes less of space. + +For GitLab we require that columns of new tables are ordered based to use the +least amount of space. An easy way of doing this is to order them based on the +type size in descending order with variable sizes (string and text columns for +example) at the end. + +## Type Sizes + +While the PostgreSQL docuemntation +(https://www.postgresql.org/docs/current/static/datatype.html) contains plenty +of information we will list the sizes of common types here so it's easier to +look them up. Here "word" refers to the word size, which is 4 bytes for a 32 +bits platform and 8 bytes for a 64 bits platform. + +| Type | Size | Aligned To | +|:-----------------|:-------------------------------------|:-----------| +| smallint | 2 bytes | 1 word | +| integer | 4 bytes | 1 word | +| bigint | 8 bytes | 8 bytes | +| real | 4 bytes | 1 word | +| double precision | 8 bytes | 8 bytes | +| boolean | 1 byte | not needed | +| text / string | variable, 1 byte plus the data | 1 word | +| bytea | variable, 1 or 4 bytes plus the data | 1 word | +| timestamp | 8 bytes | 8 bytes | +| timestamptz | 8 bytes | 8 bytes | +| date | 4 bytes | 1 word | + +A "variable" size means the actual size depends on the value being stored. If +PostgreSQL determines this can be embedded directly into a row it may do so, but +for very large values it will store the data externally and store a pointer (of +1 word in size) in the column. Because of this variable sized columns should +always be at the end of a table. + +## Real Example + +Let's use the "events" table as an example, which currently has the following +layout: + +| Column | Type | Size | +|:------------|:----------------------------|:---------| +| id | integer | 4 bytes | +| target_type | character varying | variable | +| target_id | integer | 4 bytes | +| title | character varying | variable | +| data | text | variable | +| project_id | integer | 4 bytes | +| created_at | timestamp without time zone | 8 bytes | +| updated_at | timestamp without time zone | 8 bytes | +| action | integer | 4 bytes | +| author_id | integer | 4 bytes | + +After adding padding to align the columns this would translate to columns being +divided into fixed size chunks as follows: + +| Chunk Size | Columns | +|:-----------|:------------------| +| 8 bytes | id | +| variable | target_type | +| 8 bytes | target_id | +| variable | title | +| variable | data | +| 8 bytes | project_id | +| 8 bytes | created_at | +| 8 bytes | updated_at | +| 8 bytes | action, author_id | + +This means that excluding the variable sized data we need at least 48 bytes per +row. + +We can optimise this by using the following column order instead: + +| Column | Type | Size | +|:------------|:----------------------------|:---------| +| created_at | timestamp without time zone | 8 bytes | +| updated_at | timestamp without time zone | 8 bytes | +| id | integer | 4 bytes | +| target_id | integer | 4 bytes | +| project_id | integer | 4 bytes | +| action | integer | 4 bytes | +| author_id | integer | 4 bytes | +| target_type | character varying | variable | +| title | character varying | variable | +| data | text | variable | + +This would produce the following chunks: + +| Chunk Size | Columns | +|:-----------|:-------------------| +| 8 bytes | created_at | +| 8 bytes | updated_at | +| 8 bytes | id, target_id | +| 8 bytes | project_id, action | +| 8 bytes | author_id | +| variable | target_type | +| variable | title | +| variable | data | + +Here we only need 40 bytes per row excluding the variable sized data. 8 bytes +being saved may not sound like much, but for tables as large as the "events" +table it does begin to matter. For example, when storing 80 000 000 rows this +translates to a space saving of at least 610 MB: all by just changing the order +of a few columns. diff --git a/doc/development/serializing_data.md b/doc/development/serializing_data.md index 2b56f48bc44..37332c20147 100644 --- a/doc/development/serializing_data.md +++ b/doc/development/serializing_data.md @@ -1,7 +1,8 @@ # Serializing Data **Summary:** don't store serialized data in the database, use separate columns -and/or tables instead. +and/or tables instead. This includes storing of comma separated values as a +string. Rails makes it possible to store serialized data in JSON, YAML or other formats. Such a field can be defined as follows: diff --git a/doc/development/sql.md b/doc/development/sql.md index 23fd7604957..974b1d99dff 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -216,4 +216,30 @@ exact same results. This also means there's no need to add an index on `created_at` to ensure consistent performance as `id` is already indexed by default. +## Use WHERE EXISTS instead of WHERE IN + +While `WHERE IN` and `WHERE EXISTS` can be used to produce the same data it is +recommended to use `WHERE EXISTS` whenever possible. While in many cases +PostgreSQL can optimise `WHERE IN` quite well there are also many cases where +`WHERE EXISTS` will perform (much) better. + +In Rails you have to use this by creating SQL fragments: + +```ruby +Project.where('EXISTS (?)', User.select(1).where('projects.creator_id = users.id AND users.foo = X')) +``` + +This would then produce a query along the lines of the following: + +```sql +SELECT * +FROM projects +WHERE EXISTS ( + SELECT 1 + FROM users + WHERE projects.creator_id = users.id + AND users.foo = X +) +``` + [gin-index]: http://www.postgresql.org/docs/current/static/gin.html diff --git a/doc/development/testing.md b/doc/development/testing.md index 3d5aa3d45e9..efd56484b12 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -157,8 +157,9 @@ trade-off: - Unit tests are usually cheap, and you should consider them like the basement of your house: you need them to be confident that your code is behaving - correctly. However if you run only unit tests without integration / system tests, you might [miss] the [big] [picture]! -- Integration tests are a bit more expensive, but don't abuse them. A feature test + correctly. However if you run only unit tests without integration / system + tests, you might [miss] the [big] [picture]! +- Integration tests are a bit more expensive, but don't abuse them. A system test is often better than an integration test that is stubbing a lot of internals. - System tests are expensive (compared to unit tests), even more if they require a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed) @@ -188,24 +189,34 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). ### General Guidelines - Use a single, top-level `describe ClassName` block. -- Use `described_class` instead of repeating the class name being described - (_this is enforced by RuboCop_). - Use `.method` to describe class methods and `#method` to describe instance methods. - Use `context` to test branching logic. -- Use multi-line `do...end` blocks for `before` and `after`, even when it would - fit on a single line. - Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). -- Don't supply the `:each` argument to hooks since it's the default. -- Prefer `not_to` to `to_not` (_this is enforced by RuboCop_). - Try to match the ordering of tests to the ordering within the class. - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines to separate phases. -- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` +- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` +- Don't assert against the absolute value of a sequence-generated attribute (see + [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). +- Don't supply the `:each` argument to hooks since it's the default. - On `before` and `after` hooks, prefer it scoped to `:context` over `:all` [four-phase-test]: https://robots.thoughtbot.com/four-phase-test +### Automatic retries and flaky tests detection + +On our CI, we use [rspec-retry] to automatically retry a failing example a few +times (see [`spec/spec_helper.rb`] for the precise retries count). + +We also use a home-made `RspecFlaky::Listener` listener which records flaky +examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example +is detected in any other branch (`flaky-examples-check` job). In the future, the +`flaky-examples-check` job will not be allowed to fail. + +[rspec-retry]: https://github.com/NoRedInk/rspec-retry +[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb + ### `let` variables GitLab's RSpec suite has made extensive use of `let` variables to reduce @@ -268,6 +279,43 @@ end - Avoid scenario titles that add no information, such as "successfully". - Avoid scenario titles that repeat the feature title. +### Table-based / Parameterized tests + +This style of testing is used to exercise one piece of code with a comprehensive +range of inputs. By specifying the test case once, alongside a table of inputs +and the expected output for each, your tests can be made easier to read and more +compact. + +We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized) +gem. A short example, using the table syntax and checking Ruby equality for a +range of inputs, might look like this: + +```ruby +describe "#==" do + using Rspec::Parameterized::TableSyntax + + let(:project1) { create(:project) } + let(:project2) { create(:project) } + where(:a, :b, :result) do + 1 | 1 | true + 1 | 2 | false + true | true | true + true | false | false + project1 | project1 | true + project2 | project2 | true + project 1 | project2 | false + end + + with_them do + it { expect(a == b).to eq(result) } + + it 'is isomorphic' do + expect(b == a).to eq(result) + end + end +end +``` + ### Matchers Custom matchers should be created to clarify the intent and/or hide the @@ -276,6 +324,15 @@ complexity of RSpec expectations.They should be placed under a certain type of specs only (e.g. features, requests etc.) but shouldn't be if they apply to multiple type of specs. +#### have_gitlab_http_status + +Prefer `have_gitlab_http_status` over `have_http_status` because the former +could also show the response body whenever the status mismatched. This would +be very useful whenever some tests start breaking and we would love to know +why without editing the source and rerun the tests. + +This is especially useful whenever it's showing 500 internal server error. + ### Shared contexts All shared contexts should be be placed under `spec/support/shared_contexts/`. diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md new file mode 100644 index 00000000000..cc6d62957e3 --- /dev/null +++ b/doc/development/verifying_database_capabilities.md @@ -0,0 +1,26 @@ +# Verifying Database Capabilities + +Sometimes certain bits of code may only work on a certain database and/or +version. While we try to avoid such code as much as possible sometimes it is +necessary to add database (version) specific behaviour. + +To facilitate this we have the following methods that you can use: + +* `Gitlab::Database.postgresql?`: returns `true` if PostgreSQL is being used +* `Gitlab::Database.mysql?`: returns `true` if MySQL is being used +* `Gitlab::Database.version`: returns the PostgreSQL version number as a string + in the format `X.Y.Z`. This method does not work for MySQL + +This allows you to write code such as: + +```ruby +if Gitlab::Database.postgresql? + if Gitlab::Database.version.to_f >= 9.6 + run_really_fast_query + else + run_fast_query + end +else + run_query +end +``` diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 2513f4b420a..b4b77a2f94b 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,5 +1,9 @@ # How to create a project in GitLab +>**Notes:** +- For a list of words that are not allowed to be used as project names see the + [reserved names][reserved]. + 1. In your dashboard, click the green **New project** button or use the plus icon in the upper right corner of the navigation bar. @@ -25,4 +29,12 @@ 1. Click **Create project**. +## From a template + +To kickstart your development GitLab projects can be started from a template. +For example, one of the templates included is Ruby on Rails. When filling out the +form for new projects, click the 'Ruby on Rails' button. During project creation, +this will import a Ruby on Rails template with GitLab CI preconfigured. + [import it]: ../workflow/importing/README.md +[reserved]: ../user/reserved_names.md diff --git a/doc/install/installation.md b/doc/install/installation.md index 22aedb5403e..66eb7675896 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -66,6 +66,9 @@ Install the required packages (needed to compile Ruby and native extensions to R sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake +Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but +you can [install re2 manually](https://github.com/google/re2/wiki/Install). + If you want to use Kerberos for user authentication, then install libkrb5-dev: sudo apt-get install libkrb5-dev @@ -77,7 +80,7 @@ Make sure you have the right version of Git installed # Install Git sudo apt-get install -y git-core - # Make sure Git is version 2.8.4 or higher + # Make sure Git is version 2.13.0 or higher git --version Is the system packaged Git too old? Remove it and compile from source. @@ -296,9 +299,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-4-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-5-stable gitlab -**Note:** You can change `9-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `9-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -507,15 +510,17 @@ Check if GitLab and its environment are configured correctly: sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +### Compile GetText PO files + + sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production + sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production + ### Compile Assets sudo -u git -H yarn install --production --pure-lockfile sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production -### Compile GetText PO files - - sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production - ### Start Your GitLab Instance sudo service gitlab start diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index d2442a4fbde..0fad181f59e 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,7 +1,7 @@ # GitLab Helm Chart -> Officially supported cloud providers are Google Container Service and Azure Container Service. +> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these. -> Officially supported schedulers are Kubernetes and Terraform. +> Officially supported cloud providers are Google Container Service and Azure Container Service. The `gitlab` Helm chart deploys GitLab into your Kubernetes cluster. @@ -207,7 +207,7 @@ 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 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). +to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md). >**Note:** If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size). diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md new file mode 100644 index 00000000000..bd3a85272d0 --- /dev/null +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -0,0 +1,171 @@ +# GitLab-Omnibus Helm Chart +> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these. + +> Officially supported cloud providers are Google Container Service and Azure Container Service. + +This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. + +## Introduction + +This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned as well via [Let's Encrypt](https://letsencrypt.org/). + +The deployment includes: + +- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus +- An auto-scaling [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor +- [Redis](https://github.com/kubernetes/charts/tree/master/stable/redis) +- [PostgreSQL](https://github.com/kubernetes/charts/tree/master/stable/postgresql) +- [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) +- Persistent Volume Claims for Data, Registry, Postgres, and Redis + +A video demonstration of GitLab utilizing this chart [is available](https://about.gitlab.com/handbook/sales/demo/). + +Terms: + +- Google Cloud Platform (**GCP**) +- Google Container Engine (**GKE**) +- Azure Container Service (**ACS**) +- Kubernetes (**k8s**) + +## Prerequisites + +- _At least_ 4 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 +- An [external IP address](#networking-prerequisites) +- A [wildcard DNS entry](#networking-prerequisites), which resolves to the external IP address +- The `kubectl` CLI installed locally and authenticated for the cluster +- The Helm Client installed locally +- The Helm Server (Tiller) already installed and running in the cluster, by running `helm init` +- The GitLab Helm Repo [added to your Helm Client](index.md#add-the-gitlab-helm-repository) + +### Networking Prerequisites + +This chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html) and [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/). + +To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the external Load Balancer IP. + +To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, and assigned to the Load Balancer. + +Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Please consult the documentation for your DNS service for more information on creating DNS records. + +## Configuring and Installing GitLab + +For most installations, only two parameters are required: +- `baseIP`: the desired [external IP address](#networking-prerequisites) +- `baseDomain`: the [base domain](#networking-prerequisites) with the wildcard host entry resolving to the `baseIP`. For example, `mycompany.io`. + +Other common configuration options: +- `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default. +- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart +- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for GCP, with `acs` also supported for Azure. +- `legoEmail`: Email address to use when requesting new SSL certificates from Let's Encrypt + +For additional configuration options, consult the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-omnibus/values.yaml). + +These settings can either be passed directly on the command line: +```bash +helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus +``` + +or within a YAML file: +```bash +helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus +``` + +> **Note:** +If you are using a machine type with support for less than 4 attached disks, like an Azure trial, you should disable dedicated storage for [Postgres and Redis](#persistent-storage). + +### Choosing a different GitLab release version + +The version of GitLab installed is based on the `gitlab` setting (see [section](#choosing-gitlab-edition) above), and +the value of the corresponding helm setting: `gitlabCEImage` or `gitabEEImage`. + +```yaml +gitlab: CE +gitlabCEImage: gitlab/gitlab-ce:9.1.2-ce.0 +gitlabEEImage: gitlab/gitlab-ee:9.1.2-ee.0 +``` + +The different images can be found in the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce/tags/) and [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee/tags/) +repositories on Docker Hub. + +> **Note:** +There is no guarantee that other release versions of GitLab, other than what are +used by default in the chart, will be supported by a chart install. + +### Persistent storage + +By default, persistent storage is enabled for GitLab and the charts it depends +on (Redis and PostgreSQL). + +Components can have their claim size set from your `values.yaml`, along with whether to provision separate storage for Postgres and Redis. + +Basic configuration: + +```yaml +redisImage: redis:3.2.10 +redisDedicatedStorage: true +redisStorageSize: 5Gi +postgresImage: postgres:9.6.3 +# If you disable postgresDedicatedStorage, you should consider bumping up gitlabRailsStorageSize +postgresDedicatedStorage: true +postgresStorageSize: 30Gi +gitlabRailsStorageSize: 30Gi +gitlabRegistryStorageSize: 30Gi +gitlabConfigStorageSize: 1Gi +``` + +### Routing and SSL + +Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). + +> **Note:** +Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [xip.io](http://xip.io) and [nip.io](http://nip.io) are unlikely to work. + +## 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 reviewed the [configuration settings](#configuring-and-installing-gitlab) and [added the Helm repository](index.md#add-the-gitlab-helm-repository), you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future. + +For example: +```bash +helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus +``` + +or passing them on the command line: +```bash +helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus +``` + +## Updating GitLab using the Helm Chart + +Once your GitLab Chart is installed, configuration changes and chart updates +should we done using `helm upgrade` + +```bash +helm upgrade -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab +``` + +where: + +- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom + [configuration] (#configuring-and-installing-gitlab). +- `<RELEASE-NAME>` is the name you gave the chart when installing it. + In the [Install section](#installing-gitlab-using-the-helm-chart) we called it `gitlab`. + +## Uninstalling GitLab using the Helm Chart + +To uninstall the GitLab Chart, run the following: + +```bash +helm delete <RELEASE-NAME> +``` + +where: + +- `<RELEASE-NAME>` is the name you gave the chart when installing it. + In the [Install section](#installing) we called it `gitlab`. + +[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types +[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 515b2841d08..b0fe91d6337 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,7 +1,7 @@ # GitLab Runner Helm Chart -> Officially supported cloud providers are Google Container Service and Azure Container Service. +> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these. -> Officially supported schedulers are Kubernetes and Terraform. +> Officially supported cloud providers are Google Container Service and Azure Container Service. 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 5ea08869a9b..3608aa6b2d6 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -1,7 +1,7 @@ # Installing GitLab on Kubernetes -> Officially supported cloud providers are Google Container Service and Azure Container Service. +> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these. -> Officially supported schedulers are Kubernetes, Terraform and Tectonic. +> Officially supported cloud providers are Google Container Service and Azure Container Service. 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 @@ -35,12 +35,11 @@ helm init ## Using the GitLab Helm Charts -GitLab makes available two Helm Charts, one for the GitLab server and another -for the Runner. More detailed information on installing and configuring each -Chart can be found below: +GitLab makes available three Helm Charts: an easy to use bundled chart, and a specific chart for GitLab itself and the Runner. -- [Install GitLab](gitlab_chart.md) -- [Install GitLab Runner](gitlab_runner_chart.md) +- [gitlab-omnibus](gitlab_omnibus.md): The easiest way to get started. Includes everything needed to run GitLab, including: a Runner, Container Registry, automatic SSL, and an Ingress. +- [gitlab](gitlab_chart.md): Just the GitLab service, with optional Postgres and Redis. +- [gitlab-runner](gitlab_runner_chart.md): GitLab Runner, to process CI jobs. [chart]: https://github.com/kubernetes/charts [helm-quick]: https://github.com/kubernetes/helm/blob/master/docs/quickstart.md diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 141df55f6bc..175dfc62096 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -104,6 +104,10 @@ features of GitLab work with MySQL/MariaDB: See [issue #30472][30472] for more information. 1. GitLab Geo does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). 1. [Zero downtime migrations][zero] do not work with MySQL +1. GitLab [optimizes the loading of dashboard events](https://gitlab.com/gitlab-org/gitlab-ce/issues/31806) using [PostgreSQL LATERAL JOINs](https://blog.heapanalytics.com/postgresqls-powerful-new-join-type-lateral/). +1. In general, SQL optimized for PostgreSQL may run much slower in MySQL due to + differences in query planners. For example, subqueries that work well in PostgreSQL + may not be [performant in MySQL](https://dev.mysql.com/doc/refman/5.7/en/optimizing-subqueries.html) 1. We expect this list to grow over time. Existing users using GitLab with MySQL/MariaDB are advised to diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 583ec5522fd..0399ebec86a 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -31,7 +31,7 @@ X-Gitlab-Event: System Hook "path": "storecloud", "path_with_namespace": "jsmith/storecloud", "project_id": 74, - "project_visibility": "private", + "project_visibility": "private" } ``` @@ -48,7 +48,7 @@ X-Gitlab-Event: System Hook "path": "underscore", "path_with_namespace": "jsmith/underscore", "project_id": 73, - "project_visibility": "internal", + "project_visibility": "internal" } ``` @@ -66,7 +66,7 @@ X-Gitlab-Event: System Hook "owner_name": "John Smith", "owner_email": "johnsmith@gmail.com", "project_visibility": "internal", - "old_path_with_namespace": "jsmith/overscore", + "old_path_with_namespace": "jsmith/overscore" } ``` @@ -84,7 +84,7 @@ X-Gitlab-Event: System Hook "owner_name": "John Smith", "owner_email": "johnsmith@gmail.com", "project_visibility": "internal", - "old_path_with_namespace": "jsmith/overscore", + "old_path_with_namespace": "jsmith/overscore" } ``` @@ -101,7 +101,7 @@ X-Gitlab-Event: System Hook "path": "storecloud", "path_with_namespace": "jsmith/storecloud", "project_id": 74, - "project_visibility": "private", + "project_visibility": "private" } ``` @@ -121,7 +121,7 @@ X-Gitlab-Event: System Hook "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, - "project_visibility": "private", + "project_visibility": "private" } ``` @@ -141,7 +141,7 @@ X-Gitlab-Event: System Hook "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, - "project_visibility": "private", + "project_visibility": "private" } ``` diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 4d3ababaa41..2abc57da1a0 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -264,6 +264,16 @@ sudo systemctl daemon-reload ### 9. Install libs, migrations, etc. +GitLab 9.0.11 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570) +a dependency on on the `re2` regular expression library. To install this dependency: + +```bash +sudo apt-get install libre2-dev +``` + +Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but +you can [install re2 manually](https://github.com/google/re2/wiki/Install). + ```bash cd /home/git/gitlab diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md index 2b4a7bed27f..3fd1d023d2a 100644 --- a/doc/update/9.0-to-9.1.md +++ b/doc/update/9.0-to-9.1.md @@ -264,6 +264,16 @@ sudo systemctl daemon-reload ### 9. Install libs, migrations, etc. +GitLab 9.1.8 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570) +a dependency on on the `re2` regular expression library. To install this dependency: + +```bash +sudo apt-get install libre2-dev +``` + +Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but +you can [install re2 manually](https://github.com/google/re2/wiki/Install). + ```bash cd /home/git/gitlab diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index f38547bba1a..5f7a616cc7d 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -100,6 +100,7 @@ cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale ``` For GitLab Community Edition: @@ -221,6 +222,16 @@ sudo systemctl daemon-reload ### 10. Install libs, migrations, etc. +GitLab 9.2.8 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570) +a dependency on on the `re2` regular expression library. To install this dependency: + +```bash +sudo apt-get install libre2-dev +``` + +Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but +you can [install re2 manually](https://github.com/google/re2/wiki/Install). + ```bash cd /home/git/gitlab @@ -236,6 +247,11 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production +sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production + # Update node dependencies and recompile assets sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index 373f43eb3bb..9d0b0da7edb 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -100,6 +100,7 @@ cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale ``` For GitLab Community Edition: @@ -257,6 +258,16 @@ sudo systemctl daemon-reload ### 12. Install libs, migrations, etc. +GitLab 9.3.8 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570) +a dependency on on the `re2` regular expression library. To install this dependency: + +```bash +sudo apt-get install libre2-dev +``` + +Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but +you can [install re2 manually](https://github.com/google/re2/wiki/Install). + ```bash cd /home/git/gitlab @@ -272,6 +283,10 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + # Update node dependencies and recompile assets sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md index b167f0737aa..9ee01bc9c51 100644 --- a/doc/update/9.3-to-9.4.md +++ b/doc/update/9.3-to-9.4.md @@ -100,6 +100,7 @@ cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale ``` For GitLab Community Edition: @@ -270,6 +271,16 @@ sudo systemctl daemon-reload ### 12. Install libs, migrations, etc. +GitLab 9.4 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570) +a dependency on on the `re2` regular expression library. To install this dependency: + +```bash +sudo apt-get install libre2-dev +``` + +Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but +you can [install re2 manually](https://github.com/google/re2/wiki/Install). + ```bash cd /home/git/gitlab @@ -285,6 +296,10 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + # Update node dependencies and recompile assets sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md index fc87b2d0f1e..1b5a15589af 100644 --- a/doc/update/9.4-to-9.5.md +++ b/doc/update/9.4-to-9.5.md @@ -295,6 +295,10 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + # Update node dependencies and recompile assets sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index ac1bcb8f241..30107360446 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -35,7 +35,7 @@ current version with `cat VERSION`). cd /home/git/gitlab sudo -u git -H git fetch --all -sudo -u git -H git checkout -- Gemfile.lock db/schema.rb +sudo -u git -H git checkout -- Gemfile.lock db/schema.rb locale sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG ``` @@ -56,6 +56,12 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Compile GetText PO files +# Internationalization was added in `v9.2.0` so these commands are only +# required for versions equal or major to it. +sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production +sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production + # Clean up assets and cache sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production ``` diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 08da721c71d..9e168e830e5 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -57,6 +57,10 @@ By doing so: ## Create a new group +> **Notes:** +- For a list of words that are not allowed to be used as group names see the + [reserved names][reserved]. + You can create a group in GitLab from: 1. The Groups page: expand the left menu, click **Groups**, and click the green button **New group**: @@ -153,6 +157,14 @@ Find this option under your project's settings. GitLab administrators can use the admin interface to move any project to any namespace if needed. +## Sharing a project with a group + +You can [share your projects with a group](../project/members/share_project_with_groups.md) +and give your group members access to the project all at once. + +Alternatively, with [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/), +you can [lock the sharing with group feature](#share-with-group-lock-ees-eep). + ## Manage group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. @@ -189,7 +201,7 @@ Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html# In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/) it is possible to prevent projects in a group from [sharing -a project with another group](../../workflow/share_projects_with_other_groups.md). +a project with another group](../project/members/share_project_with_groups.md). This allows for tighter control over project access. Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep). @@ -205,4 +217,5 @@ for the group (GitLab admins only, available in [GitLab Enterprise Edition Start - **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group [permissions]: ../permissions.md#permissions -[ee]: https://about.gitlab.com/products/
\ No newline at end of file +[ee]: https://about.gitlab.com/products/ +[reserved]: ../reserved_names.md diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 5724dcfab48..d2478aea4bd 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -83,10 +83,7 @@ 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 - [`path_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. + [reserved names][reserved]. To create a subgroup: @@ -175,5 +172,5 @@ 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/lib/gitlab/path_regex.rb +[reserved]: ../../reserved_names.md [issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600 diff --git a/doc/user/index.md b/doc/user/index.md index 1281cc6e4f0..d664fd62754 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -71,80 +71,44 @@ your code, use it as an issue tracker, collaborate on code, and continuously build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do it all at once, from one single project. -### Repository - -Host your codebase in [GitLab repositories](project/repository/index.md) with version control -and as part of a fully integrated platform. - -### Issues - -Explore the best of GitLab [Issues](project/issues/index.md). - -### Merge Requests - -Collanorate on code, gather reviews, live preview changes per branch, and -request approvals with [Merge Requests](project/merge_requests/index.md). - -### Milestones - -Work on multiple issues and merge requests towards the same target date -with [Milestones](project/milestones/index.md). - -### GitLab Pages - -Publish your static site directly from GitLab with [GitLab Pages](project/pages/index.md). You -can [build, test, and deploy any Static Site Generator](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) with Pages. - -### Container Registry - -Build and deploy Docker images with [GitLab Container Registry](project/container_registry.md). +- [Repositories](project/repository/index.md): Host your codebase in +repositories with version control and as part of a fully integrated platform. +- [Issues](project/issues/index.md): Explore the best of GitLab Issues' features. +- [Merge Requests](project/merge_requests/index.md): Collaborate on code, +reviews, live preview changes per branch, and request approvals with Merge Requests. +- [Milestones](project/milestones/index.md): Work on multiple issues and merge +requests towards the same target date with Milestones. ## GitLab CI/CD Use built-in [GitLab CI/CD](../ci/README.md) to test, build, and deploy your applications directly from GitLab. No third-party integrations needed. -### Auto Deploy - -Deploy your application out-of-the-box with [GitLab Auto Deploy](../ci/autodeploy/index.md). - -### Review Apps - -Live-preview the changes introduced by a merge request with [Review Apps](../ci/review_apps/index.md). - -## Groups - -With GitLab [Groups](group/index.md) you can assemble related projects together -and grant members access to several projects at once. - -### Subgroups - -Groups can also be nested in [subgroups](group/subgroups/index.md). +- [GitLab Auto Deploy](../ci/autodeploy/index.md): Deploy your application out-of-the-box with GitLab Auto Deploy. +- [Review Apps](../ci/review_apps/index.md): Live-preview the changes introduced by a merge request with Review Apps. +- [GitLab Pages](project/pages/index.md): Publish your static site directly from +GitLab with Gitlab Pages. You can build, test, and deploy any Static Site Generator with Pages. +- [GitLab Container Registry](project/container_registry.md): Build and deploy Docker +images with Container Registry. ## Account There is a lot you can customize and configure to enjoy the best of GitLab. -[Manage your user settings](profile/index.md) to change your personal info, +- [Settings](profile/index.md): Manage your user settings to change your personal info, personal access tokens, authorized applications, etc. +- [Authentication](../topics/authentication/index.md): Read through the authentication +methods available in GitLab. +- [Permissions](permissions.md): Learn the different set of permissions levels for each +user type (guest, reporter, developer, master, owner). -### Authentication - -Read through the [authentication](../topics/authentication/index.md) methods available in GitLab. - -### Permissions - -Learn the different set of [permissions](permissions.md) for user type (guest, reporter, developer, master, owner). - -## Integrations - -[Integrate GitLab](../integration/README.md) with your preferred tool, -such as Trello, JIRA, etc. +## Groups -## Git and GitLab +With GitLab [Groups](group/index.md) you can assemble related projects together +and grant members access to several projects at once. -Learn what is [Git](../topics/git/index.md) and its best practices. +Groups can also be nested in [subgroups](group/subgroups/index.md). ## Discussions @@ -168,6 +132,11 @@ requests you're assigned to. you have quick access to. You can also gather feedback on them through [discussions](#discussions). +## Integrations + +[Integrate GitLab](../integration/README.md) with your preferred tool, +such as Trello, JIRA, etc. + ## Webhooks Configure [webhooks](project/integrations/webhooks.html) to listen for @@ -178,3 +147,6 @@ POST request with data to the webhook URL. Automate GitLab via [API](../api/README.html). +## Git and GitLab + +Learn what is [Git](../topics/git/index.md) and its best practices. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 3d47e644ad2..dcf210e1085 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -5,17 +5,17 @@ particular group or project. If a user is both in a group's project and the project itself, the highest permission level is used. On public and internal projects the Guest role is not enforced. All users will -be able to create issues, leave comments, and pull or download the project code. +be able to create issues, leave comments, and clone or download the project code. -When a member leaves the team the all assigned Issues and Merge Requests +When a member leaves the team all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md) will be unassigned automatically. -GitLab administrators receive all permissions. +GitLab [administrators](../README.md#administrator-documentation) receive all permissions. -To add or import a user, you can follow the [project users and members -documentation](../workflow/add-user/add-user.md). +To add or import a user, you can follow the +[project members documentation](../user/project/members/index.md). -## Project +## Project members permissions The following table depicts the various user permission levels in a project. @@ -75,7 +75,58 @@ The following table depicts the various user permission levels in a project. | Remove protected branches [^3] | | | | | | | Remove pages | | | | | ✓ | -## Group +## Project features permissions + +### Wiki and issues + +Project features like wiki and issues can be hidden from users depending on +which visibility level you select on project settings. + +- Disabled: disabled for everyone +- Only team members: only team members will see even if your project is public or internal +- Everyone with access: everyone can see depending on your project visibility level + +### Protected branches + +To prevent people from messing with history or pushing code without +review, we've created protected branches. Read through the documentation on +[protected branches](project/protected_branches.md) +to learn more. + +Additionally, you can allow or forbid users with Master and/or +Developer permissions to push to a protected branch. Read through the documentation on +[Allowed to Merge and Allowed to Push settings](project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings) +to learn more. + +### Cycle Analytics permissions + +Find the current permissions on the Cycle Analytics dashboard on +the [documentation on Cycle Analytics permissions](project/cycle_analytics.md#permissions). + +### Issue Board permissions + +Developers and users with higher permission level can use all +the functionality of the Issue Board, that is create/delete lists +and drag issues around. Read though the +[documentation on Issue Boards permissions](project/issue_board.md#permissions) +to learn more. + +### File Locking permissions (EEP) + +The user that locks a file or directory is the only one that can edit and push their changes back to the repository where the locked objects are located. + +Read through the documentation on [permissions for File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html#permissions-on-file-locking) to learn more. + +File Locking is available in +[GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) only. + +### Confidential Issues permissions + +Confidential issues can be accessed by reporters and higher permission levels, +as well as by guest users that create a confidential issue. To learn more, +read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues). + +## Group members permissions Any user can remove themselves from a group, unless they are the last Owner of the group. The following table depicts the various user permission levels in a @@ -91,7 +142,16 @@ group. | Remove group | | | | | ✓ | | Manage group labels | | ✓ | ✓ | ✓ | ✓ | -## External Users +### Subgroup permissions + +When you add a member to a subgroup, they inherit the membership and +permission level from the parent group. This model allows access to +nested groups if you have membership in one of its parents. + +To learn more, read through the documentation on +[subgroups memberships](group/subgroups/index.md#membership). + +## External users permissions In cases where it is desired that a user has access only to some internal or private projects, there is the option of creating **External Users**. This @@ -115,18 +175,9 @@ will find the option to flag the user as external. By default new users are not set as external users. This behavior can be changed by an administrator under **Admin > Application Settings**. -## Project features - -Project features like wiki and issues can be hidden from users depending on -which visibility level you select on project settings. - -- Disabled: disabled for everyone -- Only team members: only team members will see even if your project is public or internal -- Everyone with access: everyone can see depending on your project visibility level - -## GitLab CI +## GitLab CI/CD permissions -GitLab CI permissions rely on the role the user has in GitLab. There are four +GitLab CI/CD permissions rely on the role the user has in GitLab. There are four permission levels in total: - admin @@ -134,7 +185,7 @@ permission levels in total: - developer - guest/reporter -The admin user can perform any action on GitLab CI in scope of the GitLab +The admin user can perform any action on GitLab CI/CD in scope of the GitLab instance and project. In addition, all admins can use the admin interface under `/admin/runners`. @@ -150,7 +201,7 @@ instance and project. In addition, all admins can use the admin interface under | See events in the system | | | | ✓ | | Admin interface | | | | ✓ | -### Jobs permissions +### Job permissions >**Note:** GitLab 8.12 has a completely redesigned job permissions system. @@ -174,6 +225,26 @@ users: | Push container images to current project | | ✓ | ✓ | ✓ | | Push container images to other projects | | | | | +### New CI job permissions model + +GitLab 8.12 has a completely redesigned job permissions system. To learn more, +read through the documentation on the [new CI/CD permissions model](project/new_ci_build_permissions_model.md#new-ci-job-permissions-model). + +## LDAP users permissions + +Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user. +Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/index.html#updating-user-permissions-new-feature) to learn more. + +## Auditor users permissions (EEP) + +An Auditor user should be able to access all projects and groups of a GitLab instance +with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user). + +Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) +only. + +---- + [^1]: Guest users can only view the confidential issues they created themselves [^2]: If **Public pipelines** is enabled in **Project Settings > Pipelines** [^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 91a19600951..0dd0faf35e9 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -98,7 +98,11 @@ from your fork to the upstream project - [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data) - [Importing and exporting projects between GitLab instances](settings/import_export.md) -## Leave a project +## Project's members + +Learn how to [add members to your projects](members/index.md). + +### Leave a project **Leave project** will only display on the project's dashboard when a project is part of a group (under a diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png Binary files differindex e69376f74c4..63aa0e99a50 100644 --- a/doc/user/project/integrations/img/jira_service_page.png +++ b/doc/user/project/integrations/img/jira_service_page.png diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 4f583879a4e..93aec56f8dc 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/ ## Configuration -Each GitLab project can be configured to connect to a different JIRA instance. +Each GitLab project can be configured to connect to a different JIRA instance. That +means one GitLab project maps to _all_ JIRA projects in that JIRA instance once +the configuration is set up. Therefore, you don't have to explicitly associate +one GitLab project to any JIRA project. Once the configuration is set up, any JIRA +projects in the JIRA instance are already mapped to the GitLab project. + If you have one JIRA instance you can pre-fill the settings page with a default template, see the [Services Templates][services-templates] docs. @@ -103,7 +108,6 @@ in the table below. | ----- | ----------- | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | | `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | -| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 6f15765751c..5fefb3b69c4 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -40,7 +40,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight ### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled -version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>). +version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>). 1. Read how to configure the bundled Prometheus server in the [Administration guide][gitlab-prometheus-k8s-monitor]. @@ -133,6 +133,8 @@ to integrate with. Once configured, GitLab will attempt to retrieve performance metrics for any environment which has had a successful deployment. +GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html). + [Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments) ## Determining the performance impact of a merge @@ -174,7 +176,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul [prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/ [prometheus-yml]:samples/prometheus.yml [gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434 -[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables +[ci-environment-slug]: ../../../ci/variables/#predefined-variables-environment-variables [ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935 [ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408 [promgldocs]: ../../../administration/monitoring/prometheus/index.md diff --git a/doc/user/project/integrations/prometheus_library/metrics.md b/doc/user/project/integrations/prometheus_library/metrics.md index 6bdffce9c55..f09ecf9ff2d 100644 --- a/doc/user/project/integrations/prometheus_library/metrics.md +++ b/doc/user/project/integrations/prometheus_library/metrics.md @@ -4,6 +4,7 @@ GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are: * [Kubernetes](kubernetes.md) * [NGINX](nginx.md) +* [NGINX Ingress Controller](nginx_ingress.md) * [HAProxy](haproxy.md) * [Amazon Cloud Watch](cloudwatch.md) @@ -14,10 +15,7 @@ We have tried to surface the most important metrics for each exporter, and will GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment. In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that, -GitLab will look for the required metrics which have a label that -matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug]. - -For example if you are deploying to an environment named `production`, there must be a label for the metric with the value of `production`. +GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library). ## Adding to the library diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md index b3470773996..12e3321f5f3 100644 --- a/doc/user/project/integrations/prometheus_library/nginx.md +++ b/doc/user/project/integrations/prometheus_library/nginx.md @@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro | Name | Query | | ---- | ----- | | Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) | -| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000 | -| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m])) | +| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) | +| HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) | ## Configuring Prometheus to monitor for NGINX metrics diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md new file mode 100644 index 00000000000..84ee8bc45e5 --- /dev/null +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -0,0 +1,25 @@ +# Monitoring NGINX Ingress Controller +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5 + +GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress. + +## Metrics supported + +| Name | Query | +| ---- | ----- | +| Throughput (req/sec) | sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | +| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | +| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | + +## Configuring Prometheus to monitor for NGINX ingress metrics + +The easiest way to get started is to use at least version 0.9.0 of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). If you are using NGINX as your Kubernetes ingress, there is [direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release. + +If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, these metrics will be automatically enabled and annotated for Prometheus monitoring. + +## Specifying the Environment label + +In order to isolate and only display relevant metrics for a given environment +however, GitLab needs a method to detect which labels are associated. To do this, GitLab will search metrics with appropriate labels. In this case, the `upstream` label must be of the form `<Kubernetes Namespace>-<CI_ENVIRONMENT_SLUG>-*`. + +If you have used [Auto Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part. diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/user/project/members/img/access_requests_management.png Binary files differindex 3693bed869b..3693bed869b 100644 --- a/doc/workflow/add-user/img/access_requests_management.png +++ b/doc/user/project/members/img/access_requests_management.png diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/user/project/members/img/add_new_user_to_project_settings.png Binary files differindex 40db600455f..40db600455f 100644 --- a/doc/workflow/add-user/img/add_new_user_to_project_settings.png +++ b/doc/user/project/members/img/add_new_user_to_project_settings.png diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/user/project/members/img/add_user_email_accept.png Binary files differindex 763b3ff463d..763b3ff463d 100644 --- a/doc/workflow/add-user/img/add_user_email_accept.png +++ b/doc/user/project/members/img/add_user_email_accept.png diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/user/project/members/img/add_user_email_ready.png Binary files differindex 0066eb3427b..0066eb3427b 100644 --- a/doc/workflow/add-user/img/add_user_email_ready.png +++ b/doc/user/project/members/img/add_user_email_ready.png diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/user/project/members/img/add_user_email_search.png Binary files differindex 66bcd6aad80..66bcd6aad80 100644 --- a/doc/workflow/add-user/img/add_user_email_search.png +++ b/doc/user/project/members/img/add_user_email_search.png diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/user/project/members/img/add_user_give_permissions.png Binary files differindex 376a3eefccc..376a3eefccc 100644 --- a/doc/workflow/add-user/img/add_user_give_permissions.png +++ b/doc/user/project/members/img/add_user_give_permissions.png diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/user/project/members/img/add_user_import_members_from_another_project.png Binary files differindex 0c32001098e..0c32001098e 100644 --- a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png +++ b/doc/user/project/members/img/add_user_import_members_from_another_project.png diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/user/project/members/img/add_user_imported_members.png Binary files differindex 51fd7688890..51fd7688890 100644 --- a/doc/workflow/add-user/img/add_user_imported_members.png +++ b/doc/user/project/members/img/add_user_imported_members.png diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/user/project/members/img/add_user_list_members.png Binary files differindex e0fa404288d..e0fa404288d 100644 --- a/doc/workflow/add-user/img/add_user_list_members.png +++ b/doc/user/project/members/img/add_user_list_members.png diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/user/project/members/img/add_user_members_menu.png Binary files differindex 8e61d15fe65..8e61d15fe65 100644 --- a/doc/workflow/add-user/img/add_user_members_menu.png +++ b/doc/user/project/members/img/add_user_members_menu.png diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/user/project/members/img/add_user_search_people.png Binary files differindex 41767a9167c..41767a9167c 100644 --- a/doc/workflow/add-user/img/add_user_search_people.png +++ b/doc/user/project/members/img/add_user_search_people.png diff --git a/doc/workflow/groups/max_access_level.png b/doc/user/project/members/img/max_access_level.png Binary files differindex 63f33f9d91d..63f33f9d91d 100644 --- a/doc/workflow/groups/max_access_level.png +++ b/doc/user/project/members/img/max_access_level.png diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/user/project/members/img/other_group_sees_shared_project.png Binary files differindex 67af27043eb..67af27043eb 100644 --- a/doc/workflow/groups/other_group_sees_shared_project.png +++ b/doc/user/project/members/img/other_group_sees_shared_project.png diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/user/project/members/img/request_access_button.png Binary files differindex 608baccb0ca..608baccb0ca 100644 --- a/doc/workflow/add-user/img/request_access_button.png +++ b/doc/user/project/members/img/request_access_button.png diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/user/project/members/img/share_project_with_groups.png Binary files differindex 3cb4796f9f7..3cb4796f9f7 100644 --- a/doc/workflow/groups/share_project_with_groups.png +++ b/doc/user/project/members/img/share_project_with_groups.png diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/user/project/members/img/withdraw_access_request_button.png Binary files differindex 6edd786b151..6edd786b151 100644 --- a/doc/workflow/add-user/img/withdraw_access_request_button.png +++ b/doc/user/project/members/img/withdraw_access_request_button.png diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md new file mode 100644 index 00000000000..b8dd96087f1 --- /dev/null +++ b/doc/user/project/members/index.md @@ -0,0 +1,116 @@ +# Project's members + +You can manage the groups and users and their access levels in all of your +projects. You can also personalize the access level you give each user, +per-project. + +You should have `master` or `owner` [permissions](../../permissions.md) to add +or import a new user to your project. + +To view, edit, add, and remove project's members, go to your +project's **Settings > Members**. + +--- + +## Add a user + +Right next to **People**, start typing the name or username of the user you +want to add. + +![Search for people](img/add_user_search_people.png) + +--- + +Select the user and the [permission level](../../user/permissions.md) +that you'd like to give the user. Note that you can select more than one user. + +![Give user permissions](img/add_user_give_permissions.png) + +--- + +Once done, hit **Add users to project** and they will be immediately added to +your project with the permissions you gave them above. + +![List members](img/add_user_list_members.png) + +--- + +From there on, you can either remove an existing user or change their access +level to the project. + +## Import users from another project + +You can import another project's users in your own project by hitting the +**Import members** button on the upper right corner of the **Members** menu. + +In the dropdown menu, you can see only the projects you are Master on. + +![Import members from another project](img/add_user_import_members_from_another_project.png) + +--- + +Select the one you want and hit **Import project members**. A flash message +notifying you that the import was successful will appear, and the new members +are now in the project's members list. Notice that the permissions that they +had on the project you imported from are retained. + +![Members list of new members](img/add_user_imported_members.png) + +--- + +## Invite people using their e-mail address + +If a user you want to give access to doesn't have an account on your GitLab +instance, you can invite them just by typing their e-mail address in the +user search field. + +![Invite user by mail](img/add_user_email_search.png) + +--- + +As you can imagine, you can mix inviting multiple people and adding existing +GitLab users to the project. + +![Invite user by mail ready to submit](img/add_user_email_ready.png) + +--- + +Once done, hit **Add users to project** and watch that there is a new member +with the e-mail address we used above. From there on, you can resend the +invitation, change their access level or even delete them. + +![Invite user members list](img/add_user_email_accept.png) + +--- + +Once the user accepts the invitation, they will be prompted to create a new +GitLab account using the same e-mail address the invitation was sent to. + +## Request access to a project + +As a project owner you can enable or disable non members to request access to +your project. Go to the project settings and click on **Allow users to request access**. + +As a user, you can request to be a member of a project. Go to the project you'd +like to be a member of, and click the **Request Access** button on the right +side of your screen. + +![Request access button](img/request_access_button.png) + +--- + +Project owners & masters will be notified of your request and will be able to approve or +decline it on the members page. + +![Manage access requests](img/access_requests_management.png) + +--- + +If you change your mind before your request is approved, just click the +**Withdraw Access Request** button. + +![Withdraw access request button](img/withdraw_access_request_button.png) + +## Share project with group + +Alternatively, you can [share a project with an entire group](share_project_with_groups.md) instead of adding users one by one. diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md new file mode 100644 index 00000000000..4c1ddcdcba8 --- /dev/null +++ b/doc/user/project/members/share_project_with_groups.md @@ -0,0 +1,41 @@ +# Share Projects with other Groups + +You can share projects with other [groups](../../group/index.md). This makes it +possible to add a group of users to a project with a single action. + +## Groups as collections of users + +Groups are used primarily to [create collections of projects](../user/group/index.md), but you can also +take advantage of the fact that groups define collections of _users_, namely the group +members. + +## Sharing a project with a group of users + +The primary mechanism to give a group of users, say 'Engineering', access to a project, +say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project +Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'? +This is where the group sharing feature can be of use. + +To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section. + +![The 'Groups' section in the project settings screen](img/share_project_with_groups.png) + +Now you can add the 'Engineering' group with the maximum access level of your choice. +After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard. + +!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png) + +## Maximum access level + +!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](img/max_access_level.png) + +In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. + +## Share project with group lock (EES/EEP) + +In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/) +it is possible to prevent projects in a group from [sharing +a project with another group](../members/share_project_with_groups.md). +This allows for tighter control over project access. + +Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep). diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index 4b2c435a120..5e5ae880518 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -20,6 +20,8 @@ documentation. For security reasons, when using the command line, we strongly recommend you to [connect with GitLab via SSH](../../../ssh/README.md). +## Files + ## Create and edit files Host your codebase in GitLab repositories by pushing your files to GitLab. @@ -47,6 +49,10 @@ it's easier to do so [via GitLab UI](web_editor.md): To get started with the command line, please read through the [command line basics documentation](../../../gitlab-basics/command-line-commands.md). +### Find files + +Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository. + ## Branches When you submit changes in a new branch, you create a new version diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 35960ade3d4..97cca3007b1 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -9,6 +9,9 @@ > application settings (`/admin/application_settings`) under 'Import sources'. > Ask your administrator if you don't see the **GitLab export** button when > creating a new project. +> - Starting with GitLab 10.0, administrators can disable the project export option +> on the GitLab instance in application settings (`/admin/application_settings`) +> under 'Visibility and Access Controls'. > - You can find some useful raketasks if you are an administrator in the > [import_export](../../../administration/raketasks/project_import_export.md) > raketask. diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md new file mode 100644 index 00000000000..50ec99be48b --- /dev/null +++ b/doc/user/reserved_names.md @@ -0,0 +1,109 @@ +# Reserved project and group names + +Not all project & group names are allowed because they would conflict with +existing routes used by GitLab. + +For a list of words that are not allowed to be used as group or project names, see the +[`path_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. + +## Reserved project names + +It is currently not possible to create a project with the following names: + +- - +- 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 + +## Reserved group names + +Currently the following names are reserved as top level groups: + +- 503.html +- - +- .well-known +- 404.html +- 422.html +- 500.html +- 502.html +- abuse_reports +- admin +- api +- apple-touch-icon-precomposed.png +- apple-touch-icon.png +- files +- assets +- autocomplete +- ci +- dashboard +- deploy.html +- explore +- favicon.ico +- groups +- header_logo_dark.png +- header_logo_light.png +- health_check +- help +- import +- invites +- jwt +- koding +- notification_settings +- oauth +- profile +- projects +- public +- robots.txt +- s +- search +- sent_notifications +- slash-command-logo.png +- snippets +- u +- unicorn_test +- unsubscribes +- uploads +- users + +These group names are unavailable as subgroup names: + +- - +- activity +- analytics +- audit_events +- avatar +- edit +- group_members +- hooks +- issues +- labels +- ldap +- ldap_group_links +- merge_requests +- milestones +- notification_setting +- pipeline_quota +- projects +- subgroups + +[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb diff --git a/doc/user/search/img/group_issues_filter.png b/doc/user/search/img/group_issues_filter.png Binary files differnew file mode 100644 index 00000000000..45eced79b99 --- /dev/null +++ b/doc/user/search/img/group_issues_filter.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 6d59dcc6c75..79f34fd29ba 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -40,6 +40,14 @@ The same process is valid for merge requests. Navigate to your project's **Merge and click **Search or filter results...**. Merge requests can be filtered by author, assignee, milestone, and label. +## Issues and merge requests per group + +Similar to **Issues and merge requests per project**, you can also search for issues +within a group. Navigate to a group's **Issues** tab and query search results in +the same way as you do for projects. + +![filter issues in a group](img/group_issues_filter.png) + ## 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. diff --git a/doc/user/snippets.md b/doc/user/snippets.md index 78861625f8a..2170b079f62 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -16,7 +16,7 @@ Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/is ## Project snippets -Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information. +Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information. ## Personal snippets diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 925bbf76d49..673e08287a3 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -16,14 +16,13 @@ - [File finder](file_finder.md) - [Labels](../user/project/labels.md) - [Notification emails](notifications.md) -- [Project Features](project_features.md) +- [Projects](../user/project/index.md) - [Project forking workflow](forking_workflow.md) -- [Project users](add-user/add-user.md) +- [Project users](../user/project/members/index.md) - [Protected branches](../user/project/protected_branches.md) - [Protected tags](../user/project/protected_tags.md) - [Quick Actions](../user/project/quick_actions.md) -- [Sharing a project with a group](share_with_group.md) -- [Share projects with other groups](share_projects_with_other_groups.md) +- [Sharing projects with groups](../user/project/members/share_project_with_groups.md) - [Time tracking](time_tracking.md) - [Web Editor](../user/project/repository/web_editor.md) - [Releases](releases.md) diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md index e541111d7b3..35cc080d2b7 100644 --- a/doc/workflow/add-user/add-user.md +++ b/doc/workflow/add-user/add-user.md @@ -1,114 +1 @@ -# Project users - -You can manage the groups and users and their access levels in all of your -projects. You can also personalize the access level you give each user, -per-project. - -You should have `master` or `owner` permissions to add or import a new user -to your project. - -The first step to add or import a user, go to your project and click on -**Members** in the drop-down menu on the right side of your screen. - -![Members](img/add_user_members_menu.png) - ---- - -## Add a user - -Right next to **People**, start typing the name or username of the user you -want to add. - -![Search for people](img/add_user_search_people.png) - ---- - -Select the user and the [permission level](../../user/permissions.md) -that you'd like to give the user. Note that you can select more than one user. - -![Give user permissions](img/add_user_give_permissions.png) - ---- - -Once done, hit **Add users to project** and they will be immediately added to -your project with the permissions you gave them above. - -![List members](img/add_user_list_members.png) - ---- - -From there on, you can either remove an existing user or change their access -level to the project. - -## Import users from another project - -You can import another project's users in your own project by hitting the -**Import members** button on the upper right corner of the **Members** menu. - -In the dropdown menu, you can see only the projects you are Master on. - -![Import members from another project](img/add_user_import_members_from_another_project.png) - ---- - -Select the one you want and hit **Import project members**. A flash message -notifying you that the import was successful will appear, and the new members -are now in the project's members list. Notice that the permissions that they -had on the project you imported from are retained. - -![Members list of new members](img/add_user_imported_members.png) - ---- - -## Invite people using their e-mail address - -If a user you want to give access to doesn't have an account on your GitLab -instance, you can invite them just by typing their e-mail address in the -user search field. - -![Invite user by mail](img/add_user_email_search.png) - ---- - -As you can imagine, you can mix inviting multiple people and adding existing -GitLab users to the project. - -![Invite user by mail ready to submit](img/add_user_email_ready.png) - ---- - -Once done, hit **Add users to project** and watch that there is a new member -with the e-mail address we used above. From there on, you can resend the -invitation, change their access level or even delete them. - -![Invite user members list](img/add_user_email_accept.png) - ---- - -Once the user accepts the invitation, they will be prompted to create a new -GitLab account using the same e-mail address the invitation was sent to. - -## Request access to a project - -As a project owner you can enable or disable non members to request access to -your project. Go to the project settings and click on **Allow users to request access**. - -As a user, you can request to be a member of a project. Go to the project you'd -like to be a member of, and click the **Request Access** button on the right -side of your screen. - -![Request access button](img/request_access_button.png) - ---- - -Project owners & masters will be notified of your request and will be able to approve or -decline it on the members page. - -![Manage access requests](img/access_requests_management.png) - ---- - -If you change your mind before your request is approved, just click the -**Withdraw Access Request** button. - -![Withdraw access request button](img/withdraw_access_request_button.png) +This document was moved to [../../user/project/members/index.md](../../user/project/members/index.md) diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md index 3f5de2bd4b1..feb88712f5a 100644 --- a/doc/workflow/project_features.md +++ b/doc/workflow/project_features.md @@ -1,45 +1 @@ -# Project features - -When in a Project -> Settings, you will find Features on the bottom of the page that you can toggle. - -Below you will find a more elaborate explanation of each of these. - -## Issues - -Issues is a really powerful, but lightweight issue tracking system. - -You can make tickets, assign them to people, file them under milestones, order them with labels and have discussion in them. - -They integrate deeply into GitLab and are easily referenced from anywhere by using `#` and the issue number. - -## Merge Requests - -Using a merge request, you can review and discuss code before it is merged in the branch of your code. - -As with issues, it can be assigned; people, issues, etc. can be referenced; milestones attached. - -We see it as an integral part of working together on code and couldn't work without it. - -## Wiki - -This is a separate system for documentation, built right into GitLab. - -It is source controlled and is very convenient if you don't want to keep you documentation in your source code, but you do want to keep it in your GitLab project. - -[Read more about Wikis.](../user/project/wiki/index.md) - -## Snippets - -Snippets are little bits of code or text. - -This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control. - -For example, a specific config file that is used by the team that is only valid for the people that work on the code. - -## Git LFS - ->**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins. - -Git Large File Storage allows you to easily manage large binary files with Git. -With this setting admins can better control which projects are allowed to use -LFS. +This document was moved to [../user/project/index.md](../user/project/index.md) diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md index 40d756bc199..2eb4d24958a 100644 --- a/doc/workflow/share_projects_with_other_groups.md +++ b/doc/workflow/share_projects_with_other_groups.md @@ -1,32 +1 @@ -# Share Projects with other Groups - -You can share projects with other groups. This makes it possible to add a group of users -to a project with a single action. - -## Groups as collections of users - -Groups are used primarily to [create collections of projects](../user/group/index.md), but you can also -take advantage of the fact that groups define collections of _users_, namely the group -members. - -## Sharing a project with a group of users - -The primary mechanism to give a group of users, say 'Engineering', access to a project, -say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project -Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'? -This is where the group sharing feature can be of use. - -To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section. - -![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png) - -Now you can add the 'Engineering' group with the maximum access level of your choice. -After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard. - -!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png) - -## Maximum access level - -!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png) - -In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. +This document was moved to [../user/project/members/share_project_with_groups.md](../user/project/members/share_project_with_groups.md) diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md index 3b7690973cb..2eb4d24958a 100644 --- a/doc/workflow/share_with_group.md +++ b/doc/workflow/share_with_group.md @@ -1,13 +1 @@ -# Sharing a project with a group - -If you want to share a single project in a group with another group, -you can do so easily. By setting the permission you can quickly -give a select group of users access to a project in a restricted manner. - -In a project go to the project settings -> groups. - -Now you can select a group that you want to share this project with and with -which maximum access level. Users in that group are able to access this project -with their set group access level, up to the maximum level that you've set. - -![Share a project with a group](share_with_group.png) +This document was moved to [../user/project/members/share_project_with_groups.md](../user/project/members/share_project_with_groups.md) diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb index 10ebe705365..4f44f932a6d 100644 --- a/features/steps/profile/emails.rb +++ b/features/steps/profile/emails.rb @@ -28,7 +28,7 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps expect(email).to be_nil expect(page).not_to have_content("my@email.com") end - + step 'I click link "Remove" for "my@email.com"' do # there should only be one remove button at this time click_link "Remove" diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 810cd75591b..7254fbc2e4e 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -299,9 +299,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do page.within('.diff-file:nth-of-type(5) .note') do - find('.more-actions').click - find('.more-actions .dropdown-menu li', match: :first) - find('.js-note-edit').click page.within('.current-note-edit-form', visible: true) do diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 80187b83fee..492da38355c 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -11,8 +11,8 @@ module SharedNote note = find('.note') note.hover - note.find('.more-actions').click - note.find('.more-actions .dropdown-menu li', match: :first) + find('.more-actions').click + find('.more-actions .dropdown-menu li', match: :first) find(".js-note-delete").click end @@ -147,9 +147,6 @@ module SharedNote note = find('.note') note.hover - note.find('.more-actions').click - note.find('.more-actions .dropdown-menu li', match: :first) - note.find('.js-note-edit').click end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 00f7cded2ae..605c9a3ab71 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -71,28 +71,14 @@ module SharedProject step 'project "Shop" has push event' do @project = Project.find_by(name: "Shop") - - data = { - before: Gitlab::Git::BLANK_SHA, - after: "6d394385cf567f80a8fd85055db1ab4c5295806f", - ref: "refs/heads/fix", - user_id: @user.id, - user_name: @user.name, - repository: { - name: @project.name, - url: "localhost/rubinius", - description: "", - homepage: "localhost/rubinius", - private: true - } - } - - @event = Event.create( - project: @project, - action: Event::PUSHED, - data: data, - author_id: @user.id - ) + @event = create(:push_event, project: @project, author: @user) + + create(:push_event_payload, + event: @event, + action: :created, + commit_to: '6d394385cf567f80a8fd85055db1ab4c5295806f', + ref: 'fix', + commit_count: 1) end step 'I should see project "Shop" activity feed' do diff --git a/lib/after_commit_queue.rb b/lib/after_commit_queue.rb new file mode 100644 index 00000000000..b67575a3ac2 --- /dev/null +++ b/lib/after_commit_queue.rb @@ -0,0 +1,30 @@ +module AfterCommitQueue + extend ActiveSupport::Concern + + included do + after_commit :_run_after_commit_queue + after_rollback :_clear_after_commit_queue + end + + def run_after_commit(method = nil, &block) + _after_commit_queue << proc { self.send(method) } if method + _after_commit_queue << block if block + true + end + + protected + + def _run_after_commit_queue + while action = _after_commit_queue.pop + self.instance_eval(&action) + end + end + + def _after_commit_queue + @after_commit_queue ||= [] + end + + def _clear_after_commit_queue + _after_commit_queue.clear + end +end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 0d2d71e336a..c4c0fdda665 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -122,7 +122,7 @@ module API error_classes = [MissingTokenError, TokenNotFoundError, ExpiredError, RevokedError, InsufficientScopeError] - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend end def oauth2_bearer_token_error_handler diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 6ba4005dd0b..e8dd61e493f 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -83,7 +83,7 @@ module API expose :created_at, :last_activity_at end - class Project < BasicProjectDetails + class Project < BasicProjectDetails include ::API::Helpers::RelatedResourcesHelpers expose :_links do @@ -132,7 +132,7 @@ module API expose :lfs_enabled?, as: :lfs_enabled expose :creator_id expose :namespace, using: 'API::Entities::Namespace' - expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } + expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? } expose :import_status expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } expose :avatar_url do |user, options| @@ -497,14 +497,24 @@ module API expose :author, using: Entities::UserBasic end + class PushEventPayload < Grape::Entity + expose :commit_count, :action, :ref_type, :commit_from, :commit_to + expose :ref, :commit_title + end + class Event < Grape::Entity expose :title, :project_id, :action_name expose :target_id, :target_iid, :target_type, :author_id - expose :data, :target_title + expose :target_title expose :created_at expose :note, using: Entities::Note, if: ->(event, options) { event.note? } expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author } + expose :push_event_payload, + as: :push_data, + using: PushEventPayload, + if: -> (event, _) { event.push? } + expose :author_username do |event, options| event.author&.username end @@ -531,8 +541,9 @@ module API target_url = "namespace_project_#{target_type}_url" target_anchor = "note_#{todo.note_id}" if todo.note_id? - Gitlab::Routing.url_helpers.public_send(target_url, - todo.project.namespace, todo.project, todo.target, anchor: target_anchor) + Gitlab::Routing + .url_helpers + .public_send(target_url, todo.project.namespace, todo.project, todo.target, anchor: target_anchor) # rubocop:disable GitlabSecurity/PublicSend end expose :body diff --git a/lib/api/files.rb b/lib/api/files.rb index 450334fee84..e2ac7142bc4 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,5 +1,8 @@ module API class Files < Grape::API + # Prevents returning plain/text responses for files with .txt extension + after_validation { content_type "application/json" } + helpers do def commit_params(attrs) { diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 99b8b62691f..b56fd2388b3 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -257,7 +257,15 @@ module API message << " " << trace.join("\n ") API.logger.add Logger::FATAL, message - rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) + + response_message = + if Rails.env.test? + message + else + '500 Internal Server Error' + end + + rack_response({ 'message' => response_message }.to_json, 500) end # project helpers @@ -282,7 +290,7 @@ module API def uploaded_file(field, uploads_path) if params[field] - bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename) + bad_request!("#{field} is not a file") unless params[field][:filename] return params[field] end diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index d9cae1501f8..a50ea0b52aa 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -1,8 +1,10 @@ +# rubocop:disable GitlabSecurity/PublicSend + module API module Helpers module MembersHelpers def find_source(source_type, id) - public_send("find_#{source_type}!", id) + public_send("find_#{source_type}!", id) # rubocop:disable GitlabSecurity/PublicSend end def authorize_admin_source!(source_type, source) diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 8a67de10bca..a40018b214e 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -16,9 +16,9 @@ module API case scope when String [scope] - when Hashie::Mash + when ::Hash scope.values - when Hashie::Array + when ::Array scope else ['unknown'] diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 65ff89edf65..4e4e473994b 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -139,7 +139,7 @@ module API helpers do def find_project_noteable(noteables_str, noteable_id) - public_send("find_project_#{noteables_str.singularize}", noteable_id) + public_send("find_project_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend end def noteable_read_ability_name(noteable) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 89dda88d3f5..15c3832b032 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -351,6 +351,8 @@ module API if user_project.forked_from_project.nil? user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + + ::Projects::ForksCountService.new(forked_from_project).refresh_cache else render_api_error!("Project already forked", 409) end diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index d742f2e18d0..dccf4fa27a7 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -61,7 +61,7 @@ module API service_args = [user_project, current_user, protected_branch_params] protected_branch = ::ProtectedBranches::CreateService.new(*service_args).execute - + if protected_branch.persisted? present protected_branch, with: Entities::ProtectedBranch, project: user_project else diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 5bf5a18e42f..31f940fe96b 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -153,7 +153,7 @@ module API render_api_error!('Scope contains invalid value', 400) end - runners.send(scope) + runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend end def get_runner(id) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index d55a61fa638..667ba468ce6 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -29,6 +29,7 @@ module API desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' + optional :project_export_enabled, type: Boolean, desc: 'Enable project export' optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 0fc13b35d5b..f70bc0622b7 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -57,7 +57,7 @@ module API end get "templates/licenses" do options = { - featured: declared(params).popular.present? ? true : nil + featured: declared(params)[:popular].present? ? true : nil } licences = ::Kaminari.paginate_array(Licensee::License.all(options)) present paginate(licences), with: Entities::RepoLicense @@ -71,7 +71,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do - not_found!('License') unless Licensee::License.find(declared(params).name) + not_found!('License') unless Licensee::License.find(declared(params)[:name]) template = parsed_license_template @@ -102,7 +102,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/#{template_type}/:name" do - new_template = klass.find(declared(params).name) + new_template = klass.find(declared(params)[:name]) render_response(template_type, new_template) end diff --git a/lib/api/users.rb b/lib/api/users.rb index a590f2692a2..e2019d6d512 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -79,22 +79,17 @@ module API end desc 'Get a single user' do - success Entities::UserBasic + success Entities::User end params do requires :id, type: Integer, desc: 'The ID of the user' end get ":id" do user = User.find_by(id: params[:id]) - not_found!('User') unless user + not_found!('User') unless user && can?(current_user, :read_user, user) - if current_user && current_user.admin? - present user, with: Entities::UserPublic - elsif can?(current_user, :read_user, user) - present user, with: Entities::User - else - render_api_error!("User not found.", 404) - end + opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {} + present user, opts end desc 'Create a user. Available only for admins.' do diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb index 93ad9eb26b8..c189d486f50 100644 --- a/lib/api/v3/builds.rb +++ b/lib/api/v3/builds.rb @@ -16,7 +16,7 @@ module API coerce_with: ->(scope) { if scope.is_a?(String) [scope] - elsif scope.is_a?(Hashie::Mash) + elsif scope.is_a?(::Hash) scope.values else ['unknown'] diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 773f667abe0..a9a35f2a4bd 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -25,14 +25,24 @@ module API expose(:downvote?) { |note| false } end + class PushEventPayload < Grape::Entity + expose :commit_count, :action, :ref_type, :commit_from, :commit_to + expose :ref, :commit_title + end + class Event < Grape::Entity expose :title, :project_id, :action_name expose :target_id, :target_type, :author_id - expose :data, :target_title + expose :target_title expose :created_at expose :note, using: Entities::Note, if: ->(event, options) { event.note? } expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author } + expose :push_event_payload, + as: :push_data, + using: PushEventPayload, + if: -> (event, _) { event.push? } + expose :author_username do |event, options| event.author&.username end @@ -68,7 +78,7 @@ module API expose :lfs_enabled?, as: :lfs_enabled expose :creator_id expose :namespace, using: 'API::Entities::Namespace' - expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } + expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? } expose :avatar_url do |user, options| user.avatar_url(only_path: false) end diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb index 23fe95e42e4..d49772b92f2 100644 --- a/lib/api/v3/notes.rb +++ b/lib/api/v3/notes.rb @@ -22,7 +22,7 @@ module API use :pagination end get ":id/#{noteables_str}/:noteable_id/notes" do - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend if can?(current_user, noteable_read_ability_name(noteable), noteable) # We exclude notes that are cross-references and that cannot be viewed @@ -50,7 +50,7 @@ module API requires :noteable_id, type: Integer, desc: 'The ID of the noteable' end get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend note = noteable.notes.find(params[:note_id]) can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) @@ -76,7 +76,7 @@ module API noteable_id: params[:noteable_id] } - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend if can?(current_user, noteable_read_ability_name(noteable), noteable) if params[:created_at] && (current_user.admin? || user_project.owner == current_user) diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index eb090453b48..449876c10d9 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -388,6 +388,8 @@ module API if user_project.forked_from_project.nil? user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + + ::Projects::ForksCountService.new(forked_from_project).refresh_cache else render_api_error!("Project already forked", 409) end diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb index 4c577a8d2b7..2a2fb59045c 100644 --- a/lib/api/v3/templates.rb +++ b/lib/api/v3/templates.rb @@ -59,7 +59,7 @@ module API end get route do options = { - featured: declared(params).popular.present? ? true : nil + featured: declared(params)[:popular].present? ? true : nil } present Licensee::License.all(options), with: ::API::Entities::RepoLicense end @@ -76,7 +76,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get route, requirements: { name: /[\w\.-]+/ } do - not_found!('License') unless Licensee::License.find(declared(params).name) + not_found!('License') unless Licensee::License.find(declared(params)[:name]) template = parsed_license_template @@ -111,7 +111,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get route do - new_template = klass.find(declared(params).name) + new_template = klass.find(declared(params)[:name]) render_response(template_type, new_template) end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index ca6d6848d41..b9a573d3542 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -198,11 +198,11 @@ module Backup end def archives_to_backup - ARCHIVES_TO_BACKUP.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact + ARCHIVES_TO_BACKUP.map { |name| (name + ".tar.gz") unless skipped?(name) }.compact end def folders_to_backup - FOLDERS_TO_BACKUP.reject{ |name| skipped?(name) } + FOLDERS_TO_BACKUP.reject { |name| skipped?(name) } end def disabled_features diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 53a229256a5..ed01a72ff9f 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -95,10 +95,10 @@ module Banzai private def external_issues_cached(attribute) - return project.public_send(attribute) unless RequestStore.active? + return project.public_send(attribute) unless RequestStore.active? # rubocop:disable GitlabSecurity/PublicSend cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} } - cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? + cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend cached_attributes[project.id][attribute] end end diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb index 7a81d583b82..bcb4f332267 100644 --- a/lib/banzai/filter/image_lazy_load_filter.rb +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -6,9 +6,9 @@ module Banzai doc.xpath('descendant-or-self::img').each do |img| img['class'] ||= '' << 'lazy' img['data-src'] = img['src'] - img['src'] = LazyImageTagHelper.placeholder_image + img['src'] = LazyImageTagHelper.placeholder_image end - + doc end end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index 002a3341ccd..2196a92474c 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -37,7 +37,7 @@ module Banzai objects.each_with_index do |object, index| redacted_data = redacted[index] - object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) + object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend object.user_visible_reference_count = redacted_data[:visible_reference_count] end end diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb index 321fd5bbe14..3ae3bed570d 100644 --- a/lib/banzai/pipeline/base_pipeline.rb +++ b/lib/banzai/pipeline/base_pipeline.rb @@ -18,7 +18,7 @@ module Banzai define_method(meth) do |text, context| context = transform_context(context) - html_pipeline.send(meth, text, context) + html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index ad08c0905e2..95d82d17658 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -43,7 +43,7 @@ module Banzai # Same as +render_field+, but without consulting or updating the cache field def self.cacheless_render_field(object, field, options = {}) - text = object.__send__(field) + text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend context = object.banzai_render_context(field).merge(options) cacheless_render(text, context) @@ -156,7 +156,7 @@ module Banzai # method. def self.full_cache_multi_key(cache_key, pipeline_name) return unless cache_key - Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) + Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend end # GitLab EE needs to disable updates on GET requests in Geo diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb index 3a9379ff680..a78495dbf5e 100644 --- a/lib/bitbucket/collection.rb +++ b/lib/bitbucket/collection.rb @@ -13,7 +13,7 @@ module Bitbucket def method_missing(method, *args) return super unless self.respond_to?(method) - self.send(method, *args) do |item| + self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend block_given? ? yield(item) : item end end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index 55402101e43..b9e9f9f7f4a 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -208,7 +208,7 @@ module Ci return unless command = stack.shift() if self.respond_to?("on_#{command}", true) - self.send("on_#{command}", stack) + self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend end evaluate_command_stack(stack) @@ -254,7 +254,7 @@ module Ci def state state = STATE_PARAMS.inject({}) do |h, param| - h[param] = send(param) + h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend h end Base64.urlsafe_encode64(state.to_json) @@ -266,7 +266,7 @@ module Ci return if state[:offset].to_i > stream.size STATE_PARAMS.each do |param| - send("#{param}=".to_sym, state[param]) + send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 872e418c788..76a69bf8a83 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -47,7 +47,7 @@ module Ci def collect query = project.pipelines - .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) + .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection totals_count = grouped_count(query) success_count = grouped_count(query.success) diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 4c0aee6c48f..5bef29eb1da 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -6,6 +6,8 @@ class ProjectUrlConstrainer return false unless DynamicPathValidator.valid_project_path?(full_path) + # We intentionally allow SELECT(*) here so result of this query can be used + # as cache for further Project.find_by_full_path calls within request Project.find_by_full_path(full_path, follow_redirects: request.get?).present? end end diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb index df94cafb6a1..b028169f500 100644 --- a/lib/declarative_policy/base.rb +++ b/lib/declarative_policy/base.rb @@ -109,7 +109,7 @@ module DeclarativePolicy name = name.to_sym if delegation_block.nil? - delegation_block = proc { @subject.__send__(name) } + delegation_block = proc { @subject.__send__(name) } # rubocop:disable GitlabSecurity/PublicSend end own_delegations[name] = delegation_block @@ -221,7 +221,7 @@ module DeclarativePolicy end # computes the given ability and prints a helpful debugging output - # showing which + # showing which def debug(ability, *a) runner(ability).debug(*a) end diff --git a/lib/declarative_policy/dsl.rb b/lib/declarative_policy/dsl.rb index b26807a7622..6ba1e7a3c5c 100644 --- a/lib/declarative_policy/dsl.rb +++ b/lib/declarative_policy/dsl.rb @@ -93,7 +93,7 @@ module DeclarativePolicy def method_missing(m, *a, &b) return super unless @context_class.respond_to?(m) - @context_class.__send__(m, *a, &b) + @context_class.__send__(m, *a, &b) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(m) diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index eb19ab45ac3..de391de9059 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -44,13 +44,13 @@ class FileSizeValidator < ActiveModel::EachValidator when Integer check_value when Symbol - record.send(check_value) + record.public_send(check_value) # rubocop:disable GitlabSecurity/PublicSend end value ||= [] if key == :maximum value_size = value.size - next if value_size.send(validity_check, check_value) + next if value_size.public_send(validity_check, check_value) # rubocop:disable GitlabSecurity/PublicSend errors_options = options.except(*RESERVED_OPTIONS) errors_options[:file_size] = help.number_to_human_size check_value diff --git a/lib/file_streamer.rb b/lib/file_streamer.rb deleted file mode 100644 index 4e3c6d3c773..00000000000 --- a/lib/file_streamer.rb +++ /dev/null @@ -1,16 +0,0 @@ -class FileStreamer #:nodoc: - attr_reader :to_path - - def initialize(path) - @to_path = path - end - - # Stream the file's contents if Rack::Sendfile isn't present. - def each - File.open(to_path, 'rb') do |file| - while chunk = file.read(16384) - yield chunk - end - end - end -end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 7d3aa532750..8cb4060cd97 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -101,7 +101,7 @@ module Gitlab if Service.available_services_names.include?(underscored_service) # We treat underscored_service as a trusted input because it is included # in the Service.available_services_names whitelist. - service = project.public_send("#{underscored_service}_service") + service = project.public_send("#{underscored_service}_service") # rubocop:disable GitlabSecurity/PublicSend if service && service.activated? && service.valid_token?(password) Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities) @@ -149,7 +149,7 @@ module Gitlab def abilities_for_scope(scopes) scopes.map do |scope| - self.public_send(:"#{scope}_scope_authentication_abilities") + self.public_send(:"#{scope}_scope_authentication_abilities") # rubocop:disable GitlabSecurity/PublicSend end.flatten.uniq end diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb index 1089bc9f89e..e6173d45af3 100644 --- a/lib/gitlab/auth/ip_rate_limiter.rb +++ b/lib/gitlab/auth/ip_rate_limiter.rb @@ -11,11 +11,11 @@ module Gitlab def enabled? config.enabled end - + def reset! Rack::Attack::Allow2Ban.reset(ip, config) end - + def register_fail! # Allow2Ban.filter will return false if this IP has not failed too often yet @banned = Rack::Attack::Allow2Ban.filter(ip, config) do @@ -23,17 +23,17 @@ module Gitlab ip_can_be_banned? end end - + def banned? @banned end - + private - + def config Gitlab.config.rack_attack.git_basic_auth end - + def ip_can_be_banned? config.ip_whitelist.exclude?(ip) end diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb index 0fbc6b70989..310a69a4bd4 100644 --- a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb +++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb @@ -85,6 +85,8 @@ module Gitlab diff_hash.tap do |hash| diff_text = hash[:diff] + hash[:too_large] = !!hash[:too_large] + if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only? hash[:binary] = true hash[:diff] = [diff_text].pack('m0') diff --git a/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb new file mode 100644 index 00000000000..432f7c3e706 --- /dev/null +++ b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb @@ -0,0 +1,176 @@ +module Gitlab + module BackgroundMigration + # Class that migrates events for the new push event payloads setup. All + # events are copied to a shadow table, and push events will also have a row + # created in the push_event_payloads table. + class MigrateEventsToPushEventPayloads + class Event < ActiveRecord::Base + self.table_name = 'events' + + serialize :data + + BLANK_REF = ('0' * 40).freeze + TAG_REF_PREFIX = 'refs/tags/'.freeze + MAX_INDEX = 69 + PUSHED = 5 + + def push_event? + action == PUSHED && data.present? + end + + def commit_title + commit = commits.last + + return nil unless commit && commit[:message] + + index = commit[:message].index("\n") + message = index ? commit[:message][0..index] : commit[:message] + + message.strip.truncate(70) + end + + def commit_from_sha + if create? + nil + else + data[:before] + end + end + + def commit_to_sha + if remove? + nil + else + data[:after] + end + end + + def data + super || {} + end + + def commits + data[:commits] || [] + end + + def commit_count + data[:total_commits_count] || 0 + end + + def ref + data[:ref] + end + + def trimmed_ref_name + if ref_type == :tag + ref[10..-1] + else + ref[11..-1] + end + end + + def create? + data[:before] == BLANK_REF + end + + def remove? + data[:after] == BLANK_REF + end + + def push_action + if create? + :created + elsif remove? + :removed + else + :pushed + end + end + + def ref_type + if ref.start_with?(TAG_REF_PREFIX) + :tag + else + :branch + end + end + end + + class EventForMigration < ActiveRecord::Base + self.table_name = 'events_for_migration' + end + + class PushEventPayload < ActiveRecord::Base + self.table_name = 'push_event_payloads' + + enum action: { + created: 0, + removed: 1, + pushed: 2 + } + + enum ref_type: { + branch: 0, + tag: 1 + } + end + + # start_id - The start ID of the range of events to process + # end_id - The end ID of the range to process. + def perform(start_id, end_id) + return unless migrate? + + find_events(start_id, end_id).each { |event| process_event(event) } + end + + def process_event(event) + replicate_event(event) + create_push_event_payload(event) if event.push_event? + end + + def replicate_event(event) + new_attributes = event.attributes + .with_indifferent_access.except(:title, :data) + + EventForMigration.create!(new_attributes) + rescue ActiveRecord::InvalidForeignKey + # A foreign key error means the associated event was removed. In this + # case we'll just skip migrating the event. + end + + def create_push_event_payload(event) + commit_from = pack(event.commit_from_sha) + commit_to = pack(event.commit_to_sha) + + PushEventPayload.create!( + event_id: event.id, + commit_count: event.commit_count, + ref_type: event.ref_type, + action: event.push_action, + commit_from: commit_from, + commit_to: commit_to, + ref: event.trimmed_ref_name, + commit_title: event.commit_title + ) + rescue ActiveRecord::InvalidForeignKey + # A foreign key error means the associated event was removed. In this + # case we'll just skip migrating the event. + end + + def find_events(start_id, end_id) + Event + .where('NOT EXISTS (SELECT true FROM events_for_migration WHERE events_for_migration.id = events.id)') + .where(id: start_id..end_id) + end + + def migrate? + Event.table_exists? && PushEventPayload.table_exists? && + EventForMigration.table_exists? + end + + def pack(value) + value ? [value].pack('H*') : nil + end + end + end +end diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb new file mode 100644 index 00000000000..07cec96bcc3 --- /dev/null +++ b/lib/gitlab/background_migration/move_personal_snippet_files.rb @@ -0,0 +1,79 @@ +module Gitlab + module BackgroundMigration + class MovePersonalSnippetFiles + delegate :select_all, :execute, :quote_string, to: :connection + + def perform(relative_source, relative_destination) + @source_relative_location = relative_source + @destination_relative_location = relative_destination + + move_personal_snippet_files + end + + def move_personal_snippet_files + query = "SELECT uploads.path, uploads.model_id FROM uploads "\ + "INNER JOIN snippets ON snippets.id = uploads.model_id WHERE uploader = 'PersonalFileUploader'" + select_all(query).each do |upload| + secret = upload['path'].split('/')[0] + file_name = upload['path'].split('/')[1] + + move_file(upload['model_id'], secret, file_name) + update_markdown(upload['model_id'], secret, file_name) + end + end + + def move_file(snippet_id, secret, file_name) + source_dir = File.join(base_directory, @source_relative_location, snippet_id.to_s, secret) + destination_dir = File.join(base_directory, @destination_relative_location, snippet_id.to_s, secret) + + source_file_path = File.join(source_dir, file_name) + destination_file_path = File.join(destination_dir, file_name) + + unless File.exist?(source_file_path) + say "Source file `#{source_file_path}` doesn't exist. Skipping." + return + end + + say "Moving file #{source_file_path} -> #{destination_file_path}" + + FileUtils.mkdir_p(destination_dir) + FileUtils.move(source_file_path, destination_file_path) + end + + def update_markdown(snippet_id, secret, file_name) + source_markdown_path = File.join(@source_relative_location, snippet_id.to_s, secret, file_name) + destination_markdown_path = File.join(@destination_relative_location, snippet_id.to_s, secret, file_name) + + source_markdown = "](#{source_markdown_path})" + destination_markdown = "](#{destination_markdown_path})" + quoted_source = quote_string(source_markdown) + quoted_destination = quote_string(destination_markdown) + + execute("UPDATE snippets "\ + "SET description = replace(snippets.description, '#{quoted_source}', '#{quoted_destination}'), description_html = NULL "\ + "WHERE id = #{snippet_id}") + + query = "SELECT id, note FROM notes WHERE noteable_id = #{snippet_id} "\ + "AND noteable_type = 'Snippet' AND note IS NOT NULL" + select_all(query).each do |note| + text = note['note'].gsub(source_markdown, destination_markdown) + quoted_text = quote_string(text) + + execute("UPDATE notes SET note = '#{quoted_text}', note_html = NULL WHERE id = #{note['id']}") + end + end + + def base_directory + File.join(Rails.root, 'public') + end + + def connection + ActiveRecord::Base.connection + end + + def say(message) + Rails.logger.debug(message) + end + end + end +end diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb index f1a04affd38..754a45c3257 100644 --- a/lib/gitlab/cache/request_cache.rb +++ b/lib/gitlab/cache/request_cache.rb @@ -69,7 +69,7 @@ module Gitlab instance_variable_set(ivar_name, {}) end - key = __send__(cache_key_method_name, args) + key = __send__(cache_key_method_name, args) # rubocop:disable GitlabSecurity/PublicSend store.fetch(key) { store[key] = super(*args) } end diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb index 1e73f89158d..714464fd5e7 100644 --- a/lib/gitlab/checks/force_push.rb +++ b/lib/gitlab/checks/force_push.rb @@ -5,12 +5,19 @@ module Gitlab return false if project.empty_repo? # Created or deleted branch - if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) - false - else - Gitlab::Git::RevList.new( - path_to_repo: project.repository.path_to_repo, - oldrev: oldrev, newrev: newrev).missed_ref.present? + return false if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) + + GitalyClient.migrate(:force_push) do |is_enabled| + if is_enabled + !project + .repository + .gitaly_commit_client + .is_ancestor(oldrev, newrev) + else + Gitlab::Git::RevList.new( + path_to_repo: project.repository.path_to_repo, + oldrev: oldrev, newrev: newrev).missed_ref.present? + end end end end diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index a375ccbece0..a788fb3fcbc 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -60,7 +60,7 @@ module Gitlab begin path = read_string(gz).force_encoding('UTF-8') meta = read_string(gz).force_encoding('UTF-8') - + next unless path.valid_encoding? && meta.valid_encoding? next unless path =~ match_pattern next if path =~ INVALID_PATH_PATTERN diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index 1611eba31da..d671867e7c7 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -77,8 +77,8 @@ EOM def initialize(merge_request, project) @merge_request = merge_request - @our_commit = merge_request.source_branch_head.raw.raw_commit - @their_commit = merge_request.target_branch_head.raw.raw_commit + @our_commit = merge_request.source_branch_head.raw.rugged_commit + @their_commit = merge_request.target_branch_head.raw.rugged_commit @project = project end end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index b260822788d..2479b4a7706 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -54,7 +54,7 @@ module Gitlab end def serialize_commit(event, commit, query) - commit = Commit.new(Gitlab::Git::Commit.new(commit.to_hash), @project) + commit = Commit.from_hash(commit.to_hash, @project) AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit) end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index d7dab584a44..e001d25e7b7 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -25,6 +25,10 @@ module Gitlab database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] end + def self.join_lateral_supported? + postgresql? && version.to_f >= 9.3 + end + def self.nulls_last_order(field, direction = 'ASC') order = "#{field} #{direction}" diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 2d89ccfc354..0603141e441 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -21,7 +21,7 @@ module Gitlab def to_hash hash = {} - serialize_keys.each { |key| hash[key] = send(key) } + serialize_keys.each { |key| hash[key] = send(key) } # rubocop:disable GitlabSecurity/PublicSend hash end diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb index 576a761423e..cf71d47df8e 100644 --- a/lib/gitlab/diff/line_mapper.rb +++ b/lib/gitlab/diff/line_mapper.rb @@ -38,7 +38,7 @@ module Gitlab # - The first diff line with a higher line number, if it falls between diff contexts # - The last known diff line, if it falls after the last diff context diff_line = diff_lines.find do |diff_line| - diff_from_line = diff_line.send(from) + diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend diff_from_line && diff_from_line >= from_line end diff_line ||= diff_lines.last @@ -47,8 +47,8 @@ module Gitlab # mapped line number is the same as the specified line number. return from_line unless diff_line - diff_from_line = diff_line.send(from) - diff_to_line = diff_line.send(to) + diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend + diff_to_line = diff_line.public_send(to) # rubocop:disable GitlabSecurity/PublicSend # If the line was removed, there is no mapped line number. return unless diff_to_line diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 72d7d4f84d1..abd401224d8 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -98,10 +98,11 @@ module Gitlab if status.zero? @ee_branch_found = ee_branch_prefix - else - _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}]) + return end + _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}]) + if status.zero? @ee_branch_found = ee_branch_suffix else diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index 781f9c56a42..8ddc91e341d 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -11,7 +11,7 @@ module Gitlab # obscure encoding with low confidence. # There is a lot more info with this merge request: # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193 - ENCODING_CONFIDENCE_THRESHOLD = 40 + ENCODING_CONFIDENCE_THRESHOLD = 50 def encode!(message) return nil unless message.respond_to? :force_encoding diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 8dbe25e55f6..31effdba292 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -16,7 +16,7 @@ module Gitlab def each @blames.each do |blame| yield( - Gitlab::Git::Commit.new(blame.commit), + Gitlab::Git::Commit.new(@repo, blame.commit), blame.line ) end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 77b81d2d437..7780f4e4d4f 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -54,7 +54,7 @@ module Gitlab # [[commit_sha, path], [commit_sha, path], ...]. If blob_size_limit < 0 then the # full blob contents are returned. If blob_size_limit >= 0 then each blob will # contain no more than limit bytes in its data attribute. - # + # # Keep in mind that this method may allocate a lot of memory. It is up # to the caller to limit the number of blobs and blob_size_limit. # @@ -173,7 +173,7 @@ module Gitlab def initialize(options) %w(id name path size data mode commit_id binary).each do |key| - self.send("#{key}=", options[key.to_sym]) + self.__send__("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end @loaded_all_data = false diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 600d886e818..a499bbc6266 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -14,7 +14,7 @@ module Gitlab attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator - delegate :tree, to: :raw_commit + delegate :tree, to: :rugged_commit def ==(other) return false unless other.is_a?(Gitlab::Git::Commit) @@ -50,19 +50,29 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/321 def find(repo, commit_id = "HEAD") + # Already a commit? 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) - repo.rev_parse_target(commit_id) - else - Gitlab::Git::Ref.dereference_object(commit_id) - end + # A rugged reference? + commit_id = Gitlab::Git::Ref.dereference_object(commit_id) + return decorate(repo, commit_id) if commit_id.is_a?(Rugged::Commit) - return nil unless obj.is_a?(Rugged::Commit) + # Some weird thing? + return nil unless commit_id.is_a?(String) - decorate(obj) - rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository + commit = repo.gitaly_migrate(:find_commit) do |is_enabled| + if is_enabled + repo.gitaly_commit_client.find_commit(commit_id) + else + obj = repo.rev_parse_target(commit_id) + + obj.is_a?(Rugged::Commit) ? obj : nil + end + end + + decorate(repo, commit) if commit + rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, + Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository nil end @@ -102,7 +112,7 @@ module Gitlab if is_enabled repo.gitaly_commit_client.between(base, head) else - repo.rugged_commits_between(base, head).map { |c| decorate(c) } + repo.rugged_commits_between(base, head).map { |c| decorate(repo, c) } end end rescue Rugged::ReferenceError @@ -169,7 +179,7 @@ module Gitlab offset = actual_options[:skip] limit = actual_options[:max_count] walker.each(offset: offset, limit: limit) do |commit| - commits.push(decorate(commit)) + commits.push(decorate(repo, commit)) end walker.reset @@ -183,27 +193,8 @@ module Gitlab Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options) end - def decorate(commit, ref = nil) - Gitlab::Git::Commit.new(commit, ref) - end - - # Returns a diff object for the changes introduced by +rugged_commit+. - # If +rugged_commit+ doesn't have a parent, then the diff is between - # this commit and an empty repo. See Repository#diff for the keys - # allowed in the +options+ hash. - def diff_from_parent(rugged_commit, options = {}) - options ||= {} - break_rewrites = options[:break_rewrites] - actual_options = Gitlab::Git::Diff.filter_diff_options(options) - - diff = if rugged_commit.parents.empty? - rugged_commit.diff(actual_options.merge(reverse: true)) - else - rugged_commit.parents[0].diff(rugged_commit, actual_options) - end - - diff.find_similar!(break_rewrites: break_rewrites) - diff + def decorate(repository, commit, ref = nil) + Gitlab::Git::Commit.new(repository, commit, ref) end # Returns the `Rugged` sorting type constant for one or more given @@ -219,9 +210,19 @@ module Gitlab @rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE) end + + def shas_with_signatures(repository, shas) + shas.select do |sha| + begin + Rugged::Commit.extract_signature(repository.rugged, sha) + rescue Rugged::OdbError + false + end + end + end end - def initialize(raw_commit, head = nil) + def initialize(repository, raw_commit, head = nil) raise "Nil as raw commit passed" unless raw_commit case raw_commit @@ -229,12 +230,13 @@ module Gitlab init_from_hash(raw_commit) when Rugged::Commit init_from_rugged(raw_commit) - when Gitlab::GitalyClient::Commit + when Gitaly::GitCommit init_from_gitaly(raw_commit) else raise "Invalid raw commit type: #{raw_commit.class}" end + @repository = repository @head = head end @@ -269,19 +271,50 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324 def to_diff - diff_from_parent.patch + rugged_diff_from_parent.patch end # Returns a diff object for the changes from this commit's first parent. # If there is no parent, then the diff is between this commit and an - # empty repo. See Repository#diff for keys allowed in the +options+ + # empty repo. See Repository#diff for keys allowed in the +options+ # hash. def diff_from_parent(options = {}) - Commit.diff_from_parent(raw_commit, options) + Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled| + if is_enabled + @repository.gitaly_commit_client.diff_from_parent(self, options) + else + rugged_diff_from_parent(options) + end + end + end + + def rugged_diff_from_parent(options = {}) + options ||= {} + break_rewrites = options[:break_rewrites] + actual_options = Gitlab::Git::Diff.filter_diff_options(options) + + diff = if rugged_commit.parents.empty? + rugged_commit.diff(actual_options.merge(reverse: true)) + else + rugged_commit.parents[0].diff(rugged_commit, actual_options) + end + + diff.find_similar!(break_rewrites: break_rewrites) + diff end def deltas - @deltas ||= diff_from_parent.each_delta.map { |d| Gitlab::Git::Diff.new(d) } + @deltas ||= begin + deltas = Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled| + if is_enabled + @repository.gitaly_commit_client.commit_deltas(self) + else + rugged_diff_from_parent.each_delta + end + end + + deltas.map { |delta| Gitlab::Git::Diff.new(delta) } + end end def has_zero_stats? @@ -296,7 +329,7 @@ module Gitlab def to_hash serialize_keys.map.with_object({}) do |key, hash| - hash[key] = send(key) + hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend end end @@ -309,23 +342,7 @@ module Gitlab end def parents - case raw_commit - when Rugged::Commit - raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) } - when Gitlab::GitalyClient::Commit - parent_ids.map { |oid| self.class.find(raw_commit.repository, oid) }.compact - else - raise NotImplementedError, "commit source doesn't support #parents" - end - end - - # Get the gpg signature of this commit. - # - # Ex. - # commit.signature(repo) - # - def signature(repo) - Rugged::Commit.extract_signature(repo.rugged, sha) + parent_ids.map { |oid| self.class.find(@repository, oid) }.compact end def stats @@ -334,7 +351,7 @@ module Gitlab def to_patch(options = {}) begin - raw_commit.to_mbox(options) + rugged_commit.to_mbox(options) rescue Rugged::InvalidError => ex if ex.message =~ /commit \w+ is a merge commit/i 'Patch format is not currently supported for merge commits.' @@ -382,13 +399,21 @@ module Gitlab encode! @committer_email end + def rugged_commit + @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit) + raw_commit + else + @repository.rev_parse_target(id) + end + end + private def init_from_hash(hash) raw_commit = hash.symbolize_keys serialize_keys.each do |key| - send("#{key}=", raw_commit[key]) + send("#{key}=", raw_commit[key]) # rubocop:disable GitlabSecurity/PublicSend end end @@ -415,10 +440,10 @@ module Gitlab # subject from the message to make it clearer when there's one # available but not the other. @message = (commit.body.presence || commit.subject).dup - @authored_date = Time.at(commit.author.date.seconds) + @authored_date = Time.at(commit.author.date.seconds).utc @author_name = commit.author.name.dup @author_email = commit.author.email.dup - @committed_date = Time.at(commit.committer.date.seconds) + @committed_date = Time.at(commit.committer.date.seconds).utc @committer_name = commit.committer.name.dup @committer_email = commit.committer.email.dup @parent_ids = commit.parent_ids diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb index 57c29ad112c..00acb4763e9 100644 --- a/lib/gitlab/git/commit_stats.rb +++ b/lib/gitlab/git/commit_stats.rb @@ -16,7 +16,7 @@ module Gitlab @deletions = 0 @total = 0 - diff = commit.diff_from_parent + diff = commit.rugged_diff_from_parent diff.each_patch do |p| # TODO: Use the new Rugged convenience methods when they're released diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 9e00abefd02..ce3d65062e8 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -143,7 +143,7 @@ module Gitlab hash = {} SERIALIZE_KEYS.each do |key| - hash[key] = send(key) + hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend end hash @@ -221,7 +221,7 @@ module Gitlab raw_diff = hash.symbolize_keys SERIALIZE_KEYS.each do |key| - send(:"#{key}=", raw_diff[key.to_sym]) + send(:"#{key}=", raw_diff[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 87ed9c3ea26..6a601561c2a 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -28,7 +28,6 @@ module Gitlab @limits = self.class.collection_limits(options) @enforce_limits = !!options.fetch(:limits, true) @expanded = !!options.fetch(:expanded, true) - @from_gitaly = options.fetch(:from_gitaly, false) @line_count = 0 @byte_count = 0 @@ -44,7 +43,7 @@ module Gitlab return if @iterator.nil? Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled| - if is_enabled && @from_gitaly + if is_enabled && @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher) each_gitaly_patch(&block) else each_rugged_patch(&block) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index f246393cfbc..1d5ca68137a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -18,6 +18,28 @@ module Gitlab InvalidBlobName = Class.new(StandardError) InvalidRef = Class.new(StandardError) + class << self + # Unlike `new`, `create` takes the storage path, not the storage name + def create(storage_path, name, bare: true, symlink_hooks_to: nil) + repo_path = File.join(storage_path, name) + repo_path += '.git' unless repo_path.end_with?('.git') + + FileUtils.mkdir_p(repo_path, mode: 0770) + + # Equivalent to `git --git-path=#{repo_path} init [--bare]` + repo = Rugged::Repository.init_at(repo_path, bare) + repo.close + + if symlink_hooks_to.present? + hooks_path = File.join(repo_path, 'hooks') + FileUtils.rm_rf(hooks_path) + FileUtils.ln_s(symlink_hooks_to, hooks_path) + end + + true + end + end + # Full path to repo attr_reader :path @@ -321,7 +343,24 @@ module Gitlab options[:limit] ||= 0 options[:offset] ||= 0 - raw_log(options).map { |c| Commit.decorate(c) } + raw_log(options).map { |c| Commit.decorate(self, c) } + end + + # Used in gitaly-ruby + def raw_log(options) + actual_ref = options[:ref] || root_ref + begin + sha = sha_from_ref(actual_ref) + rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError + # Return an empty array if the ref wasn't found + return [] + end + + if log_using_shell?(options) + log_by_shell(sha, options) + else + log_by_walk(sha, options) + end end def count_commits(options) @@ -603,29 +642,13 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/327 def ls_files(ref) - actual_ref = ref || root_ref - - begin - sha_from_ref(actual_ref) - rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError - # Return an empty array if the ref wasn't found - return [] - end - - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) - cmd += %w(-r) - cmd += %w(--full-tree) - cmd += %w(--full-name) - cmd += %W(-- #{actual_ref}) - - raw_output = IO.popen(cmd, &:read).split("\n").map do |f| - stuff, path = f.split("\t") - _mode, type, _sha = stuff.split(" ") - path if type == "blob" - # Contain only blob type + gitaly_migrate(:ls_files) do |is_enabled| + if is_enabled + gitaly_ls_files(ref) + else + git_ls_files(ref) + end end - - raw_output.compact end # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/328 @@ -733,22 +756,6 @@ module Gitlab sort_branches(branches, sort_by) end - def raw_log(options) - actual_ref = options[:ref] || root_ref - begin - sha = sha_from_ref(actual_ref) - rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError - # Return an empty array if the ref wasn't found - return [] - end - - if log_using_shell?(options) - log_by_shell(sha, options) - else - log_by_walk(sha, options) - end - end - def log_using_shell?(options) options[:path].present? || options[:disable_walk] || @@ -826,6 +833,8 @@ module Gitlab return unless commit_object && commit_object.type == :COMMIT gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) + return unless gitmodules + found_module = GitmodulesParser.new(gitmodules.data).parse[path] found_module && found_module['url'] @@ -973,6 +982,36 @@ module Gitlab raw_output.to_i end + + def gitaly_ls_files(ref) + gitaly_commit_client.ls_files(ref) + end + + def git_ls_files(ref) + actual_ref = ref || root_ref + + begin + sha_from_ref(actual_ref) + rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError + # Return an empty array if the ref wasn't found + return [] + end + + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) + cmd += %w(-r) + cmd += %w(--full-tree) + cmd += %w(--full-name) + cmd += %W(-- #{actual_ref}) + + raw_output = IO.popen(cmd, &:read).split("\n").map do |f| + stuff, path = f.split("\t") + _mode, type, _sha = stuff.split(" ") + path if type == "blob" + # Contain only blob type + end + + raw_output.compact + end end end end diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index 8e959c57c7c..b54962a4456 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -89,7 +89,7 @@ module Gitlab def initialize(options) %w(id root_id name path type mode commit_id).each do |key| - self.send("#{key}=", options[key.to_sym]) + self.send("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index c90ef282fdd..9a5f4f598b2 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -55,7 +55,7 @@ module Gitlab def self.call(storage, service, rpc, request) metadata = request_metadata(storage) metadata = yield(metadata) if block_given? - stub(service, storage).send(rpc, request, metadata) + stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend end def self.request_metadata(storage) @@ -100,5 +100,9 @@ module Gitlab path = Rails.root.join(SERVER_VERSION_FILE) path.read.chomp end + + def self.encode(s) + s.dup.force_encoding(Encoding::ASCII_8BIT) + end end end diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb index 7ea8e8d0857..a250eb75bd4 100644 --- a/lib/gitlab/gitaly_client/blob_service.rb +++ b/lib/gitlab/gitaly_client/blob_service.rb @@ -13,10 +13,17 @@ module Gitlab ) response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request) - blob = response.first - return unless blob.oid.present? + data = '' + blob = nil + response.each do |msg| + if blob.nil? + blob = msg + end - data = response.reduce(blob.data.dup) { |memo, msg| memo << msg.data.dup } + data << msg.data + end + + return nil if blob.oid.blank? Gitlab::Git::Blob.new( id: blob.oid, diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb deleted file mode 100644 index 61fe462d762..00000000000 --- a/lib/gitlab/gitaly_client/commit.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Gitlab - module GitalyClient - class Commit - attr_reader :repository, :gitaly_commit - - delegate :id, :subject, :body, :author, :committer, :parent_ids, to: :gitaly_commit - - def initialize(repository, gitaly_commit) - @repository = repository - @gitaly_commit = gitaly_commit - end - end - end -end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 3f577ac8530..b36e81278d6 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -10,6 +10,18 @@ module Gitlab @repository = repository end + def ls_files(revision) + request = Gitaly::ListFilesRequest.new( + repository: @gitaly_repo, + revision: GitalyClient.encode(revision) + ) + + response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request) + response.flat_map do |msg| + msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) } + end + end + def is_ancestor(ancestor_id, child_id) request = Gitaly::CommitIsAncestorRequest.new( repository: @gitaly_repo, @@ -29,35 +41,40 @@ module Gitlab request = Gitaly::CommitDiffRequest.new(request_params) response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request) - Gitlab::Git::DiffCollection.new(GitalyClient::DiffStitcher.new(response), options.merge(from_gitaly: true)) + GitalyClient::DiffStitcher.new(response) end def commit_deltas(commit) request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit)) response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request) - response.flat_map do |msg| - msg.deltas.map { |d| Gitlab::Git::Diff.new(d) } - end + + response.flat_map { |msg| msg.deltas } end def tree_entry(ref, path, limit = nil) request = Gitaly::TreeEntryRequest.new( repository: @gitaly_repo, revision: ref, - path: path.dup.force_encoding(Encoding::ASCII_8BIT), + path: GitalyClient.encode(path), limit: limit.to_i ) response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request) - entry = response.first - return unless entry.oid.present? - if entry.type == :BLOB - rest_of_data = response.reduce("") { |memo, msg| memo << msg.data } - entry.data += rest_of_data + entry = nil + data = '' + response.each do |msg| + if entry.nil? + entry = msg + + break unless entry.type == :BLOB + end + + data << msg.data end + entry.data = data - entry + entry unless entry.oid.blank? end def tree_entries(repository, revision, path) @@ -100,15 +117,14 @@ module Gitlab def last_commit_for_path(revision, path) request = Gitaly::LastCommitForPathRequest.new( repository: @gitaly_repo, - revision: revision.force_encoding(Encoding::ASCII_8BIT), - path: path.to_s.force_encoding(Encoding::ASCII_8BIT) + revision: GitalyClient.encode(revision), + path: GitalyClient.encode(path.to_s) ) gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit return unless gitaly_commit - commit = GitalyClient::Commit.new(@repository, gitaly_commit) - Gitlab::Git::Commit.new(commit) + Gitlab::Git::Commit.new(@repository, gitaly_commit) end def between(from, to) @@ -167,10 +183,21 @@ module Gitlab response.reduce("") { |memo, msg| memo << msg.data } end + def find_commit(revision) + request = Gitaly::FindCommitRequest.new( + repository: @gitaly_repo, + revision: GitalyClient.encode(revision) + ) + + response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request) + + response.commit + end + private def commit_diff_request_params(commit, options = {}) - parent_id = commit.parents[0]&.id || EMPTY_TREE_ID + parent_id = commit.parent_ids.first || EMPTY_TREE_ID { repository: @gitaly_repo, @@ -183,8 +210,7 @@ module Gitlab def consume_commits_response(response) response.flat_map do |message| message.commits.map do |gitaly_commit| - commit = GitalyClient::Commit.new(@repository, gitaly_commit) - Gitlab::Git::Commit.new(commit) + Gitlab::Git::Commit.new(@repository, gitaly_commit) end end end diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb index d459c9a88fb..54df6304865 100644 --- a/lib/gitlab/gitaly_client/diff.rb +++ b/lib/gitlab/gitaly_client/diff.rb @@ -7,13 +7,13 @@ module Gitlab def initialize(params) params.each do |key, val| - public_send(:"#{key}=", val) + public_send(:"#{key}=", val) # rubocop:disable GitlabSecurity/PublicSend end end def ==(other) FIELDS.all? do |field| - public_send(field) == other.public_send(field) + public_send(field) == other.public_send(field) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index b0f7548b7dc..919fb68b8c7 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -16,8 +16,7 @@ module Gitlab response.flat_map do |message| message.branches.map do |branch| - gitaly_commit = GitalyClient::Commit.new(@repository, branch.target) - target_commit = Gitlab::Git::Commit.decorate(gitaly_commit) + target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target) Gitlab::Git::Branch.new(@repository, branch.name, branch.target.id, target_commit) end end @@ -102,8 +101,7 @@ module Gitlab response.flat_map do |message| message.tags.map do |gitaly_tag| if gitaly_tag.target_commit.present? - commit = GitalyClient::Commit.new(@repository, gitaly_tag.target_commit) - gitaly_commit = Gitlab::Git::Commit.new(commit) + gitaly_commit = Gitlab::Git::Commit.decorate(@repository, gitaly_tag.target_commit) end Gitlab::Git::Tag.new( @@ -141,7 +139,7 @@ module Gitlab committer_email: response.commit_committer.email.dup } - Gitlab::Git::Commit.decorate(hash) + Gitlab::Git::Commit.decorate(@repository, hash) end end end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 79ce784f2f2..6ad97e62941 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -10,7 +10,7 @@ module Gitlab def exists? request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo) - GitalyClient.call(@storage, :repository_service, :exists, request).exists + GitalyClient.call(@storage, :repository_service, :repository_exists, request).exists end def garbage_collect(create_bitmap) diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index f5a4c5493ef..8fc937496af 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -5,7 +5,9 @@ module Gitlab def repository(repository_storage, relative_path) Gitaly::Repository.new( storage_name: repository_storage, - relative_path: relative_path + relative_path: relative_path, + git_object_directory: Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].to_s, + git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']) ) end end diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 8c80791e7c9..f330041cc00 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -11,7 +11,9 @@ module Gitlab end def create! - project.public_send(project_association).find_or_create_by!(find_condition) do |record| + association = project.public_send(project_association) # rubocop:disable GitlabSecurity/PublicSend + + association.find_or_create_by!(find_condition) do |record| record.attributes = attributes end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 7dbeec5b010..0550f9695bd 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -120,7 +120,7 @@ module Gitlab def request(method, *args, &block) sleep rate_limit_sleep_time if rate_limit_exceed? - data = api.send(method, *args) + data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend return data unless data.is_a?(Array) last_response = api.last_response diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 266b1a6fece..373062b354b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -289,7 +289,7 @@ module Gitlab opts.last[:page] = current_page(resource_type) - client.public_send(resource_type, *opts) do |resources| + client.public_send(resource_type, *opts) do |resources| # rubocop:disable GitlabSecurity/PublicSend yield resources increment_page(resource_type) end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index 86fb6c51765..f1007daab5d 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -71,7 +71,7 @@ module Gitlab end def config - Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"} + Gitlab.config.omniauth.providers.find {|provider| provider.name == "gitlab"} end def gitlab_options diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index e1d1724295a..45e9f9d65ae 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -2,6 +2,8 @@ module Gitlab module Gpg extend self + MUTEX = Mutex.new + module CurrentKeyChain extend self @@ -42,21 +44,37 @@ module Gitlab end end - def using_tmp_keychain - Dir.mktmpdir do |dir| - @original_dirs ||= [GPGME::Engine.dirinfo('homedir')] - @original_dirs.push(dir) - - GPGME::Engine.home_dir = dir - - return_value = yield + # Allows thread safe switching of temporary keychain files + # + # 1. The current thread may use nesting of temporary keychain + # 2. Another thread needs to wait for the lock to be released + def using_tmp_keychain(&block) + if MUTEX.locked? && MUTEX.owned? + optimistic_using_tmp_keychain(&block) + else + MUTEX.synchronize do + optimistic_using_tmp_keychain(&block) + end + end + end - @original_dirs.pop + # 1. Returns the custom home directory if one has been set by calling + # `GPGME::Engine.home_dir=` + # 2. Returns the default home directory otherwise + def current_home_dir + GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir') + end - GPGME::Engine.home_dir = @original_dirs[-1] + private - return_value + def optimistic_using_tmp_keychain + previous_dir = current_home_dir + Dir.mktmpdir do |dir| + GPGME::Engine.home_dir = dir + yield end + ensure + GPGME::Engine.home_dir = previous_dir end end end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 55428b85207..606c7576f70 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -1,12 +1,20 @@ module Gitlab module Gpg class Commit - attr_reader :commit + def self.for_commit(commit) + new(commit.project, commit.sha) + end - def initialize(commit) - @commit = commit + def initialize(project, sha) + @project = project + @sha = sha - @signature_text, @signed_text = commit.raw.signature(commit.project.repository) + @signature_text, @signed_text = + begin + Rugged::Commit.extract_signature(project.repository.rugged, sha) + rescue Rugged::OdbError + nil + end end def has_signature? @@ -16,18 +24,20 @@ module Gitlab def signature return unless has_signature? - cached_signature = GpgSignature.find_by(commit_sha: commit.sha) - return cached_signature if cached_signature.present? + return @signature if @signature - using_keychain do |gpg_key| - create_cached_signature!(gpg_key) - end + cached_signature = GpgSignature.find_by(commit_sha: @sha) + return @signature = cached_signature if cached_signature.present? + + @signature = create_cached_signature! end def update_signature!(cached_signature) using_keychain do |gpg_key| cached_signature.update_attributes!(attributes(gpg_key)) end + + @signature = cached_signature end private @@ -55,16 +65,18 @@ module Gitlab end end - def create_cached_signature!(gpg_key) - GpgSignature.create!(attributes(gpg_key)) + def create_cached_signature! + using_keychain do |gpg_key| + GpgSignature.create!(attributes(gpg_key)) + end end def attributes(gpg_key) user_infos = user_infos(gpg_key) { - commit_sha: commit.sha, - project: commit.project, + commit_sha: @sha, + project: @project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, gpg_key_user_name: user_infos[:name], diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 3bb491120ba..a525ee7a9ee 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -10,9 +10,7 @@ module Gitlab .select(:id, :commit_sha, :project_id) .where('gpg_key_id IS NULL OR valid_signature = ?', false) .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) - .find_each do |gpg_signature| - Gitlab::Gpg::Commit.new(gpg_signature.commit).update_signature!(gpg_signature) - end + .find_each { |sig| sig.gpg_commit.update_signature!(sig) } end end end diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index d230de781d5..56042ddecbf 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -1,7 +1,6 @@ module Gitlab module ImportExport class AttributesFinder - def initialize(included_attributes:, excluded_attributes:, methods:) @included_attributes = included_attributes || {} @excluded_attributes = excluded_attributes || {} diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index ffd17118c91..989342389bc 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -47,12 +47,16 @@ module Gitlab end def remove_symlinks! - Dir["#{@shared.export_path}/**/*"].each do |path| + extracted_files.each do |path| FileUtils.rm(path) if File.lstat(path).symlink? end true end + + def extracted_files + Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ /.*\/\.{1,2}$/ } + end end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index c5c05bfe2fb..9d9ebcb389a 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -3,18 +3,22 @@ project_tree: - labels: :priorities - milestones: - - :events + - events: + - :push_event_payload - issues: - - :events + - events: + - :push_event_payload - :timelogs - notes: - :author - - :events + - events: + - :push_event_payload - label_links: - label: :priorities - milestone: - - :events + - events: + - :push_event_payload - snippets: - :award_emoji - notes: @@ -25,21 +29,25 @@ project_tree: - merge_requests: - notes: - :author - - :events + - events: + - :push_event_payload - merge_request_diff: - :merge_request_diff_commits - :merge_request_diff_files - - :events + - events: + - :push_event_payload - :timelogs - label_links: - label: :priorities - milestone: - - :events + - events: + - :push_event_payload - pipelines: - notes: - :author - - :events + - events: + - :push_event_payload - :stages - :statuses - :triggers @@ -107,6 +115,8 @@ excluded_attributes: statuses: - :trace - :token + push_event_payload: + - :event_id methods: labels: diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 84ab1977dfa..cbc8d170936 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -1,6 +1,9 @@ module Gitlab module ImportExport class ProjectTreeRestorer + # Relations which cannot have both group_id and project_id at the same time + RESTRICT_PROJECT_AND_GROUP = %i(milestones).freeze + def initialize(user:, shared:, project:) @path = File.join(shared.export_path, 'project.json') @user = user @@ -118,9 +121,11 @@ module Gitlab end def create_relation(relation, relation_hash_list) + relation_type = relation.to_sym + relation_array = [relation_hash_list].flatten.map do |relation_hash| - Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, - relation_hash: parsed_relation_hash(relation_hash), + Gitlab::ImportExport::RelationFactory.create(relation_sym: relation_type, + relation_hash: parsed_relation_hash(relation_hash, relation_type), members_mapper: members_mapper, user: @user, project: restored_project) @@ -129,8 +134,16 @@ module Gitlab relation_hash_list.is_a?(Array) ? relation_array : relation_array.first end - def parsed_relation_hash(relation_hash) - relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id) + def parsed_relation_hash(relation_hash, relation_type) + if RESTRICT_PROJECT_AND_GROUP.include?(relation_type) + params = {} + params['group_id'] = restored_project.group.try(:id) if relation_hash['group_id'] + params['project_id'] = restored_project.id if relation_hash['project_id'] + else + params = { 'group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id } + end + + relation_hash.merge(params) end end end diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb index 2a659ae4c74..99594577141 100644 --- a/lib/gitlab/lazy.rb +++ b/lib/gitlab/lazy.rb @@ -16,7 +16,7 @@ module Gitlab def method_missing(name, *args, &block) __evaluate__ - @result.__send__(name, *args, &block) + @result.__send__(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(name, include_private = false) diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index 95378e5a769..4fbc5fa5262 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -17,7 +17,7 @@ module Gitlab value = value.first if value break if value.present? end - + return super unless value Gitlab::Utils.force_utf8(value) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 43eb73250b7..e138b466a34 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -32,7 +32,7 @@ module Gitlab end def uid - entry.send(config.uid).first + entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend end def username @@ -65,7 +65,7 @@ module Gitlab return nil unless selected_attr - entry.public_send(selected_attr) + entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb index 699d8b9fc07..306923902e0 100644 --- a/lib/gitlab/markdown/pipeline.rb +++ b/lib/gitlab/markdown/pipeline.rb @@ -23,7 +23,7 @@ module Gitlab define_method(meth) do |text, context| context = transform_context(context) - html_pipeline.send(meth, text, context) + html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb index 5d2d7d0026c..63c3372da51 100644 --- a/lib/gitlab/middleware/rails_queue_duration.rb +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -8,7 +8,7 @@ module Gitlab def initialize(app) @app = app end - + def call(env) trans = Gitlab::Metrics.current_transaction proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb index 6105d165810..6aecf63231f 100644 --- a/lib/gitlab/middleware/webpack_proxy.rb +++ b/lib/gitlab/middleware/webpack_proxy.rb @@ -1,6 +1,7 @@ # This Rack middleware is intended to proxy the webpack assets directory to the # webpack-dev-server. It is only intended for use in development. +# :nocov: module Gitlab module Middleware class WebpackProxy < Rack::Proxy @@ -22,3 +23,4 @@ module Gitlab end end end +# :nocov: diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb index f33bfd0bd0e..30739f2a2c5 100644 --- a/lib/gitlab/o_auth/session.rb +++ b/lib/gitlab/o_auth/session.rb @@ -1,3 +1,4 @@ +# :nocov: module Gitlab module OAuth module Session @@ -15,3 +16,4 @@ module Gitlab end end end +# :nocov: diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index cf461adf697..732fbf68dad 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -25,7 +25,9 @@ module Gitlab end TEMPLATES_TABLE = [ - ProjectTemplate.new('rails', 'Ruby on Rails') + ProjectTemplate.new('rails', 'Ruby on Rails'), + ProjectTemplate.new('spring', 'Spring'), + ProjectTemplate.new('express', 'NodeJS Express') ].freeze class << self diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb index b0da516ff83..9bf019b72e6 100644 --- a/lib/gitlab/redis/cache.rb +++ b/lib/gitlab/redis/cache.rb @@ -7,9 +7,6 @@ module Gitlab CACHE_NAMESPACE = 'cache:gitlab'.freeze DEFAULT_REDIS_CACHE_URL = 'redis://localhost:6380'.freeze REDIS_CACHE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CACHE_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_CACHE_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.cache.yml').freeze - end class << self def default_url @@ -22,7 +19,7 @@ module Gitlab return file_name unless file_name.nil? # otherwise, if config files exists for this class, use it - file_name = File.expand_path(DEFAULT_REDIS_CACHE_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('redis.cache.yml') return file_name if File.file?(file_name) # this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent diff --git a/lib/gitlab/redis/queues.rb b/lib/gitlab/redis/queues.rb index f9249d05565..e1695aafbeb 100644 --- a/lib/gitlab/redis/queues.rb +++ b/lib/gitlab/redis/queues.rb @@ -8,9 +8,6 @@ module Gitlab MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze DEFAULT_REDIS_QUEUES_URL = 'redis://localhost:6381'.freeze REDIS_QUEUES_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_QUEUES_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_QUEUES_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.queues.yml').freeze - end class << self def default_url @@ -23,7 +20,7 @@ module Gitlab return file_name if file_name # otherwise, if config files exists for this class, use it - file_name = File.expand_path(DEFAULT_REDIS_QUEUES_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('redis.queues.yml') return file_name if File.file?(file_name) # this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb index 395dcf082da..10bec7a90da 100644 --- a/lib/gitlab/redis/shared_state.rb +++ b/lib/gitlab/redis/shared_state.rb @@ -7,9 +7,6 @@ module Gitlab SESSION_NAMESPACE = 'session:gitlab'.freeze DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_SHARED_STATE_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.shared_state.yml').freeze - end class << self def default_url @@ -22,7 +19,7 @@ module Gitlab return file_name if file_name # otherwise, if config files exists for this class, use it - file_name = File.expand_path(DEFAULT_REDIS_SHARED_STATE_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('redis.shared_state.yml') return file_name if File.file?(file_name) # this will force use of DEFAULT_REDIS_SHARED_STATE_URL when config file is absent diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb index c43b37dde74..8ad06480575 100644 --- a/lib/gitlab/redis/wrapper.rb +++ b/lib/gitlab/redis/wrapper.rb @@ -8,9 +8,6 @@ module Gitlab class Wrapper DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze REDIS_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_CONFIG_FILE_NAME = ::Rails.root.join('config', 'resque.yml').freeze - end class << self delegate :params, :url, to: :new @@ -49,13 +46,21 @@ module Gitlab DEFAULT_REDIS_URL end + # Return the absolute path to a Rails configuration file + # + # We use this instead of `Rails.root` because for certain tasks + # utilizing these classes, `Rails` might not be available. + def config_file_path(filename) + File.expand_path("../../../config/#{filename}", __dir__) + end + def config_file_name # if ENV set for wrapper class, use it even if it points to a file does not exist file_name = ENV[REDIS_CONFIG_ENV_VAR_NAME] return file_name unless file_name.nil? # otherwise, if config files exists for wrapper class, use it - file_name = File.expand_path(DEFAULT_REDIS_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('resque.yml') return file_name if File.file?(file_name) # nil will force use of DEFAULT_REDIS_URL when config file is absent diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index f5b757ace77..bc836dcc08d 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -45,7 +45,7 @@ module Gitlab end def all - REFERABLES.each { |referable| send(referable.to_s.pluralize) } + REFERABLES.each { |referable| send(referable.to_s.pluralize) } # rubocop:disable GitlabSecurity/PublicSend @references.values.flatten end diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 823f697f51c..f9ab9bd466f 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -1,3 +1,4 @@ +# :nocov: module DeliverNever def deliver_later self @@ -21,3 +22,4 @@ module Gitlab end end end +# :nocov: diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 0cb28732402..280a9abf03e 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -73,8 +73,10 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def add_repository(storage, name) - gitlab_shell_fast_execute([gitlab_shell_projects_path, - 'add-project', storage, "#{name}.git"]) + Gitlab::Git::Repository.create(storage, name, bare: true, symlink_hooks_to: gitlab_shell_hooks_path) + rescue => err + Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}") + false end # Import repository @@ -273,7 +275,11 @@ module Gitlab protected def gitlab_shell_path - Gitlab.config.gitlab_shell.path + File.expand_path(Gitlab.config.gitlab_shell.path) + end + + def gitlab_shell_hooks_path + File.expand_path(Gitlab.config.gitlab_shell.hooks_path) end def gitlab_shell_user_home diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index ca8d3271541..a0a2769cf9e 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -90,9 +90,14 @@ module Gitlab # # Returns an array of completed JIDs def self.completed_jids(job_ids) - Sidekiq.redis do |redis| - job_ids.reject { |jid| redis.exists(key_for(jid)) } + statuses = job_status(job_ids) + + completed = [] + job_ids.zip(statuses).each do |job_id, status| + completed << job_id unless status end + + completed end def self.key_for(jid) diff --git a/lib/gitlab/slash_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb index ea611a4d629..ab855319077 100644 --- a/lib/gitlab/slash_commands/presenters/help.rb +++ b/lib/gitlab/slash_commands/presenters/help.rb @@ -14,7 +14,7 @@ module Gitlab if text.start_with?('help') header_with_list("Available commands", full_commands(trigger)) else - header_with_list("Unknown command, these commands are available", full_commands(trigger)) + header_with_list("Unknown command, these commands are available", full_commands(trigger)) end end diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 7e14a566696..fee1a127fd7 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -19,6 +19,8 @@ module Gitlab return false if internal?(uri) return true if blocked_port?(uri.port) + return true if blocked_user_or_hostname?(uri.user) + return true if blocked_user_or_hostname?(uri.hostname) server_ips = Resolv.getaddresses(uri.hostname) return true if (blocked_ips & server_ips).any? @@ -37,6 +39,12 @@ module Gitlab port < 1024 && !VALID_PORTS.include?(port) end + def blocked_user_or_hostname?(value) + return false if value.blank? + + value !~ /\A\p{Alnum}/ + end + def internal?(uri) internal_web?(uri) || internal_shell?(uri) end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index fa182c4deda..9670c93759e 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -14,6 +14,19 @@ module Gitlab str.force_encoding(Encoding::UTF_8) end + # A slugified version of the string, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen + def slugify(str) + return str.downcase + .gsub(/[^a-z0-9]/, '-')[0..62] + .gsub(/(\A-+|-+\z)/, '') + end + def to_boolean(value) return value if [true, false].include?(value) return true if value =~ /^(true|t|yes|y|1|on)$/i diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 3f25e463412..a362a3a0bc6 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -45,7 +45,6 @@ module Gitlab raise "Unsupported action: #{action}" end if feature_enabled - params[:GitalyAddress] = server[:address] # This field will be deprecated params[:GitalyServer] = server end diff --git a/lib/rspec_flaky/example.rb b/lib/rspec_flaky/example.rb new file mode 100644 index 00000000000..b6e790cbbab --- /dev/null +++ b/lib/rspec_flaky/example.rb @@ -0,0 +1,46 @@ +module RspecFlaky + # This is a wrapper class for RSpec::Core::Example + class Example + delegate :status, :exception, to: :execution_result + + def initialize(rspec_example) + @rspec_example = rspec_example.try(:example) || rspec_example + end + + def uid + @uid ||= Digest::MD5.hexdigest("#{description}-#{file}") + end + + def example_id + rspec_example.id + end + + def file + metadata[:file_path] + end + + def line + metadata[:line_number] + end + + def description + metadata[:full_description] + end + + def attempts + rspec_example.try(:attempts) || 1 + end + + private + + attr_reader :rspec_example + + def metadata + rspec_example.metadata + end + + def execution_result + rspec_example.execution_result + end + end +end diff --git a/lib/rspec_flaky/flaky_example.rb b/lib/rspec_flaky/flaky_example.rb new file mode 100644 index 00000000000..f81fb90e870 --- /dev/null +++ b/lib/rspec_flaky/flaky_example.rb @@ -0,0 +1,39 @@ +module RspecFlaky + # This represents a flaky RSpec example and is mainly meant to be saved in a JSON file + class FlakyExample < OpenStruct + def initialize(example) + if example.respond_to?(:example_id) + super( + example_id: example.example_id, + file: example.file, + line: example.line, + description: example.description, + last_attempts_count: example.attempts, + flaky_reports: 1) + else + super + end + end + + def first_flaky_at + self[:first_flaky_at] || Time.now + end + + def last_flaky_at + Time.now + end + + def last_flaky_job + return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID'] + + "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}" + end + + def to_h + super.merge( + first_flaky_at: first_flaky_at, + last_flaky_at: last_flaky_at, + last_flaky_job: last_flaky_job) + end + end +end diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb new file mode 100644 index 00000000000..ec2fbd9e36c --- /dev/null +++ b/lib/rspec_flaky/listener.rb @@ -0,0 +1,75 @@ +require 'json' + +module RspecFlaky + class Listener + attr_reader :all_flaky_examples, :new_flaky_examples + + def initialize + @new_flaky_examples = {} + @all_flaky_examples = init_all_flaky_examples + end + + def example_passed(notification) + current_example = RspecFlaky::Example.new(notification.example) + + return unless current_example.attempts > 1 + + flaky_example_hash = all_flaky_examples[current_example.uid] + + all_flaky_examples[current_example.uid] = + if flaky_example_hash + FlakyExample.new(flaky_example_hash).tap do |ex| + ex.last_attempts_count = current_example.attempts + ex.flaky_reports += 1 + end + else + FlakyExample.new(current_example).tap do |ex| + new_flaky_examples[current_example.uid] = ex + end + end + end + + def dump_summary(_) + write_report_file(all_flaky_examples, all_flaky_examples_report_path) + + if new_flaky_examples.any? + Rails.logger.warn "\nNew flaky examples detected:\n" + Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples)) + + write_report_file(new_flaky_examples, new_flaky_examples_report_path) + end + end + + def to_report(examples) + Hash[examples.map { |k, ex| [k, ex.to_h] }] + end + + private + + def init_all_flaky_examples + return {} unless File.exist?(all_flaky_examples_report_path) + + all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path)) + + Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }] + end + + def write_report_file(examples, file_path) + return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true' + + report_path_dir = File.dirname(file_path) + FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir) + File.write(file_path, JSON.pretty_generate(to_report(examples))) + end + + def all_flaky_examples_report_path + @all_flaky_examples_report_path ||= ENV['ALL_FLAKY_RSPEC_REPORT_PATH'] || + Rails.root.join("rspec_flaky/all-report.json") + end + + def new_flaky_examples_report_path + @new_flaky_examples_report_path ||= ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || + Rails.root.join("rspec_flaky/new-report.json") + end + end +end diff --git a/lib/static_model.rb b/lib/static_model.rb index 185921d8fbe..60e2dd82e4e 100644 --- a/lib/static_model.rb +++ b/lib/static_model.rb @@ -18,7 +18,7 @@ module StaticModel # # Pass it along if we respond to it. def [](key) - send(key) if respond_to?(key) + send(key) if respond_to?(key) # rubocop:disable GitlabSecurity/PublicSend end def to_param diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index f25e66d54c8..54f51d9d633 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -25,6 +25,39 @@ map $http_upgrade $connection_upgrade_gitlab { '' close; } +## NGINX 'combined' log format with filtered query strings +log_format gitlab_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent"; + +## Remove private_token from the request URI +# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... +map $request_uri $gitlab_temp_request_uri_1 { + default $request_uri; + ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; +} + +## Remove authenticity_token from the request URI +# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... +map $gitlab_temp_request_uri_1 $gitlab_temp_request_uri_2 { + default $gitlab_temp_request_uri_1; + ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; +} + +## Remove rss_token from the request URI +# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&... +map $gitlab_temp_request_uri_2 $gitlab_filtered_request_uri { + default $gitlab_temp_request_uri_2; + ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; +} + +## A version of the referer without the query string +map $http_referer $gitlab_filtered_http_referer { + default $http_referer; + ~^(?<temp>.*)\? $temp; +} + ## Normal HTTP host server { ## Either remove "default_server" from the listen line below, @@ -46,7 +79,7 @@ server { # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 ## Individual nginx logs for this GitLab vhost - access_log /var/log/nginx/gitlab_access.log; + access_log /var/log/nginx/gitlab_access.log gitlab_access; error_log /var/log/nginx/gitlab_error.log; location / { diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index d9746c5c1aa..875c8bcbf3c 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -18,8 +18,11 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache off; + # The same address as passed to GitLab Pages: `-listen-proxy` - proxy_pass http://localhost:8090/; + proxy_pass http://localhost:8090/; } # Define custom error pages diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl index a1ccf266835..62ed482e2bf 100644 --- a/lib/support/nginx/gitlab-pages-ssl +++ b/lib/support/nginx/gitlab-pages-ssl @@ -67,8 +67,11 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache off; + # The same address as passed to GitLab Pages: `-listen-proxy` - proxy_pass http://localhost:8090/; + proxy_pass http://localhost:8090/; } # Define custom error pages diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 2b40da18bab..ed8131ef24f 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -29,6 +29,41 @@ map $http_upgrade $connection_upgrade_gitlab_ssl { '' close; } + +## NGINX 'combined' log format with filtered query strings +log_format gitlab_ssl_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent"; + +## Remove private_token from the request URI +# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... +map $request_uri $gitlab_ssl_temp_request_uri_1 { + default $request_uri; + ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; +} + +## Remove authenticity_token from the request URI +# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... +map $gitlab_ssl_temp_request_uri_1 $gitlab_ssl_temp_request_uri_2 { + default $gitlab_ssl_temp_request_uri_1; + ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; +} + +## Remove rss_token from the request URI +# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&... +map $gitlab_ssl_temp_request_uri_2 $gitlab_ssl_filtered_request_uri { + default $gitlab_ssl_temp_request_uri_2; + ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; +} + +## A version of the referer without the query string +map $http_referer $gitlab_ssl_filtered_http_referer { + default $http_referer; + ~^(?<temp>.*)\? $temp; +} + + ## Redirects all HTTP traffic to the HTTPS host server { ## Either remove "default_server" from the listen line below, @@ -40,7 +75,7 @@ server { server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice return 301 https://$http_host$request_uri; - access_log /var/log/nginx/gitlab_access.log; + access_log /var/log/nginx/gitlab_access.log gitlab_ssl_access; error_log /var/log/nginx/gitlab_error.log; } @@ -93,7 +128,7 @@ server { # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 ## Individual nginx logs for this GitLab vhost - access_log /var/log/nginx/gitlab_access.log; + access_log /var/log/nginx/gitlab_access.log gitlab_ssl_access; error_log /var/log/nginx/gitlab_error.log; location / { diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index dbb3b827b9a..1bd36bbe20a 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -41,8 +41,6 @@ namespace :gitlab do end namespace :gitlab_shell do - include SystemCheck::Helpers - desc "GitLab | Check the configuration of GitLab Shell" task check: :environment do warn_user_is_not_gitlab @@ -249,8 +247,6 @@ namespace :gitlab do end namespace :sidekiq do - include SystemCheck::Helpers - desc "GitLab | Check the configuration of Sidekiq" task check: :environment do warn_user_is_not_gitlab @@ -309,8 +305,6 @@ namespace :gitlab do end namespace :incoming_email do - include SystemCheck::Helpers - desc "GitLab | Check the configuration of Reply by email" task check: :environment do warn_user_is_not_gitlab @@ -444,8 +438,6 @@ namespace :gitlab do end namespace :ldap do - include SystemCheck::Helpers - task :check, [:limit] => :environment do |_, args| # Only show up to 100 results because LDAP directories can be very big. # This setting only affects the `rake gitlab:check` script. @@ -501,8 +493,6 @@ namespace :gitlab do end namespace :repo do - include SystemCheck::Helpers - desc "GitLab | Check the integrity of the repositories managed by GitLab" task check: :environment do Gitlab.config.repositories.storages.each do |name, repository_storage| @@ -517,8 +507,6 @@ namespace :gitlab do end namespace :user do - include SystemCheck::Helpers - desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| username = args[:username] || prompt("Check repository integrity for fsername? ".color(:blue)) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index aaf00bd703a..08677a98fc1 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -15,13 +15,17 @@ namespace :gitlab do checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir) _, status = Gitlab::Popen.popen(%w[which gmake]) - command = status.zero? ? 'gmake' : 'make' + command = status.zero? ? ['gmake'] : ['make'] + + if Rails.env.test? + command += %W[BUNDLE_PATH=#{Bundler.bundle_path}] + end Dir.chdir(args.dir) do create_gitaly_configuration # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? - Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT] + [command]) } + Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + command) } end end end diff --git a/lib/tasks/gitlab/helpers.rake b/lib/tasks/gitlab/helpers.rake index dd2d5861481..b0a24790c4a 100644 --- a/lib/tasks/gitlab/helpers.rake +++ b/lib/tasks/gitlab/helpers.rake @@ -4,5 +4,5 @@ require 'tasks/gitlab/task_helpers' StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] namespace :gitlab do - include Gitlab::TaskHelpers + extend SystemCheck::Helpers end diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb index 28b2d86eed2..d85b810ac66 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/tasks/gitlab/task_helpers.rb @@ -5,6 +5,8 @@ module Gitlab TaskAbortedByUserError = Class.new(StandardError) module TaskHelpers + extend self + # Ask if the user wants to continue # # Returns "yes" the user chose to continue diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index a7e30423c7a..f44abc2b81b 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -21,13 +21,18 @@ namespace :gitlab do params = { import_url: template.clone_url, namespace_id: admin.namespace.id, - path: template.title, + path: template.name, skip_wiki: true } - puts "Creating project for #{template.name}" + puts "Creating project for #{template.title}" project = Projects::CreateService.new(admin, params).execute + unless project.persisted? + puts project.errors.messages + exit(1) + end + loop do if project.finished? puts "Import finished for #{template.name}" diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb index 41dee5fdc06..4a3c40f88eb 100644 --- a/lib/uploaded_file.rb +++ b/lib/uploaded_file.rb @@ -27,7 +27,7 @@ class UploadedFile alias_method :local_path, :path def method_missing(method_name, *args, &block) #:nodoc: - @tempfile.__send__(method_name, *args, &block) + @tempfile.__send__(method_name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to?(method_name, include_private = false) #:nodoc: diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po index 85d806e6f20..5c531f0cd7d 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -1163,22 +1163,11 @@ msgstr "Няма достатъчно данни за този етап." msgid "Withdraw Access Request" msgstr "Оттегляне на заявката за достъп" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"На път сте да премахнете „%{group_name}“.\n" -"Ако я премахнете, групата НЕ може да бъде възстановена!\n" -"НАИСТИНА ли искате това?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "На път сте да премахнете „%{group_name}“. Ако я премахнете, групата НЕ може да бъде възстановена! НАИСТИНА ли искате това?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"На път сте да премахнете „%{project_name_with_namespace}“.\n" -"Ако го премахнете, той НЕ може да бъде възстановен!\n" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "На път сте да премахнете „%{project_name_with_namespace}“. Ако го премахнете, той НЕ може да бъде възстановен!" "НАИСТИНА ли искате това?" msgid "" diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index 46bf4e33997..0ac591d4927 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,17 +17,34 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + msgid "%s additional commit has been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues." msgstr[0] "" msgstr[1] "" -msgid "%d commit" -msgid_plural "%d commits" +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "" + +msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." +msgstr "" + +msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." +msgstr "" + +msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." +msgstr "" + +msgid "%{storage_name}: failed storage access attempt on host:" +msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" msgstr[0] "" msgstr[1] "" -msgid "%{commit_author_link} committed %{commit_timeago}" +msgid "(checkout the %{link} for information on how to install it)." msgstr "" msgid "1 pipeline" @@ -41,6 +58,9 @@ msgstr "" msgid "About auto deploy" msgstr "" +msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "" + msgid "Active" msgstr "" @@ -68,6 +88,18 @@ msgstr "" msgid "Are you sure you want to delete this pipeline schedule?" msgstr "" +msgid "Are you sure you want to discard your changes?" +msgstr "" + +msgid "Are you sure you want to reset registration token?" +msgstr "" + +msgid "Are you sure you want to reset the health check token?" +msgstr "" + +msgid "Are you sure?" +msgstr "" + msgid "Attach a file by drag & drop or %{upload_link}" msgstr "" @@ -109,6 +141,9 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Cancel edit" +msgstr "" + msgid "ChangeTypeActionLabel|Pick into branch" msgstr "" @@ -234,6 +269,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a new branch" +msgstr "" + msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "" @@ -311,9 +349,15 @@ msgstr[1] "" msgid "Description" msgstr "" +msgid "Details" +msgstr "" + msgid "Directory name" msgstr "" +msgid "Discard changes" +msgstr "" + msgid "Don't show again" msgstr "" @@ -397,12 +441,36 @@ msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "Git storage health information has been reset" +msgstr "" + +msgid "GitLab Runner section" +msgstr "" + msgid "Go to your fork" msgstr "" msgid "GoToYourFork|Fork" msgstr "" +msgid "Health Check" +msgstr "" + +msgid "Health information can be retrieved from the following endpoints. More information is available" +msgstr "" + +msgid "HealthCheck|Access token is" +msgstr "" + +msgid "HealthCheck|Healthy" +msgstr "" + +msgid "HealthCheck|No Health Problems Detected" +msgstr "" + +msgid "HealthCheck|Unhealthy" +msgstr "" + msgid "Home" msgstr "" @@ -412,6 +480,9 @@ msgstr "" msgid "Import repository" msgstr "" +msgid "Install a Runner compatible with GitLab CI" +msgstr "" + msgid "Interval Pattern" msgstr "" @@ -470,6 +541,9 @@ msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" +msgid "More information is available|here" +msgstr "" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "" @@ -682,6 +756,9 @@ msgstr "" msgid "Project access must be granted explicitly to each user." msgstr "" +msgid "Project details" +msgstr "" + msgid "Project export could not be deleted." msgstr "" @@ -754,9 +831,21 @@ msgstr "" msgid "Remove project" msgstr "" +msgid "Repository" +msgstr "" + msgid "Request Access" msgstr "" +msgid "Reset git storage health information" +msgstr "" + +msgid "Reset health check access token" +msgstr "" + +msgid "Reset runners registration token" +msgstr "" + msgid "Revert this commit" msgstr "" @@ -781,6 +870,9 @@ msgstr "" msgid "Select a timezone" msgstr "" +msgid "Select existing branch" +msgstr "" + msgid "Select target branch" msgstr "" @@ -807,12 +899,18 @@ msgstr[1] "" msgid "Source code" msgstr "" +msgid "Specify the following URL during the Runner setup:" +msgstr "" + msgid "StarProject|Star" msgstr "" msgid "Start a %{new_merge_request} with these changes" msgstr "" +msgid "Start the Runner!" +msgstr "" + msgid "Switch branch/tag" msgstr "" @@ -875,6 +973,9 @@ msgstr "" msgid "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." msgstr "" +msgid "There are problems accessing Git storage: " +msgstr "" + msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "" @@ -1044,6 +1145,9 @@ msgstr "" msgid "UploadLink|click to upload" msgstr "" +msgid "Use the following registration token during setup:" +msgstr "" + msgid "Use your global notification setting" msgstr "" @@ -1059,6 +1163,9 @@ msgstr "" msgid "VisibilityLevel|Public" msgstr "" +msgid "VisibilityLevel|Unknown" +msgstr "" + msgid "Want to see the data? Please ask an administrator for access." msgstr "" @@ -1068,16 +1175,10 @@ msgstr "" msgid "Withdraw Access Request" msgstr "" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po index d688478972d..94ae131186b 100644 --- a/locale/eo/gitlab.po +++ b/locale/eo/gitlab.po @@ -1165,30 +1165,14 @@ msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon." msgid "Withdraw Access Request" msgstr "Nuligi la peton pri atingeblo" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vi forigos „%{group_name}“.\n" -"Oni NE POVAS malfari la forigon de grupo!\n" -"Ĉu vi estas ABSOLUTE certa?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vi forigos „%{group_name}“. Oni NE POVAS malfari la forigon de grupo! Ĉu vi estas ABSOLUTE certa?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vi forigos „%{project_name_with_namespace}“.\n" -"Oni NE POVAS malfari la forigon de projekto!\n" -"Ĉu vi estas ABSOLUTE certa?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vi forigos „%{project_name_with_namespace}“. Oni NE POVAS malfari la forigon de projekto! Ĉu vi estas ABSOLUTE certa?" -msgid "" -"You are going to remove the fork relationship to source project " -"%{forked_from_project}. Are you ABSOLUTELY sure?" -msgstr "" -"Vi forigos la rilaton de la disbranĉigo al la originala projekto, " -"„%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?" +msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "Vi forigos la rilaton de la disbranĉigo al la originala projekto, „%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?" msgid "" "You are going to transfer %{project_name_with_namespace} to another owner. " diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 5c669d51a68..e43fd5fea15 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -1071,23 +1071,11 @@ msgstr "No hay suficientes datos para mostrar en esta etapa." msgid "Withdraw Access Request" msgstr "Retirar Solicitud de Acceso" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Va a eliminar %{group_name}.\n" -"¡El grupo eliminado NO puede ser restaurado!\n" -"¿Estás TOTALMENTE seguro?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Va a eliminar %{group_name}. ¡El grupo eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Va a eliminar %{project_name_with_namespace}.\n" -"¡El proyecto eliminado NO puede ser restaurado!\n" -"¿Estás TOTALMENTE seguro?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Va a eliminar %{project_name_with_namespace}. ¡El proyecto eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?" diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po index c490933c6d4..83f31f7a3b2 100644 --- a/locale/fr/gitlab.po +++ b/locale/fr/gitlab.po @@ -417,7 +417,7 @@ msgstr[0] "Fourche" msgstr[1] "Fourches" msgid "ForkedFromProjectPath|Forked from" -msgstr "Fouché depuis" +msgstr "Fourché depuis" msgid "From issue creation until deploy to production" msgstr "Depuis la création de l'incident jusqu'au déploiement en production" @@ -1175,22 +1175,11 @@ msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape." msgid "Withdraw Access Request" msgstr "Retirer la demande d'accès" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE " -"PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vous êtes sur le point de supprimer %{project_name_with_namespace}.\n" -"Les projets supprimés NE PEUVENT PAS être restaurés !\n" -"Êtes vous ABSOLUMENT sûr ? " +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vous êtes sur le point de supprimer %{project_name_with_namespace}. Les projets supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/gitlab.pot b/locale/gitlab.pot index babef3ed0af..e60504e1395 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1072,16 +1072,10 @@ msgstr "" msgid "Withdraw Access Request" msgstr "" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index 7ba23d84405..e719a3988e3 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -1169,23 +1169,11 @@ msgstr "Non ci sono sufficienti dati da mostrare su questo stadio" msgid "Withdraw Access Request" msgstr "Ritira richiesta d'accesso" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Stai per rimuovere il gruppo %{group_name}.\n" -"I gruppi rimossi NON possono esser ripristinati!\n" -"Sei ASSOLUTAMENTE sicuro?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Stai per rimuovere il gruppo %{group_name}. I gruppi rimossi NON POSSONO esser ripristinati! Sei ASSOLUTAMENTE sicuro?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Stai per rimuovere %{project_name_with_namespace}.\n" -"I progetti rimossi NON POSSONO essere ripristinati\n" -"Sei assolutamente sicuro?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Stai per rimuovere %{project_name_with_namespace}. I progetti rimossi NON POSSONO essere ripristinati! Sei assolutamente sicuro?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po index 0b1db651c11..bfa97aa21d7 100644 --- a/locale/ja/gitlab.po +++ b/locale/ja/gitlab.po @@ -1119,22 +1119,11 @@ msgstr "データ不足のため、このステージの表示はできません msgid "Withdraw Access Request" msgstr "アクセスリクエストを取り消す" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "%{group_name} グループを削除しようとしています。\n" -"削除されたグループは絶対に元に戻せません!\n" -"本当によろしいですか?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{group_name} グループを削除しようとしています。 削除されたグループは絶対に元に戻せません!本当によろしいですか?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"%{project_name_with_namespace} プロジェクトを削除しようとしています。\n" -"削除されたプロジェクトは絶対に元には戻せません!\n" -"本当によろしいですか?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{project_name_with_namespace} プロジェクトを削除しようとしています。削除されたプロジェクトは絶対に元には戻せません!本当によろしいですか?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po index 97a844ada7f..340c8955d20 100644 --- a/locale/ko/gitlab.po +++ b/locale/ko/gitlab.po @@ -1,4 +1,7 @@ -# chang-ho,cha <changho.cha@gmail.com>, 2017. #zanata +# Korean translations for gitlab package. +# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2017. # Huang Tao <htve@outlook.com>, 2017. #zanata msgid "" msgstr "" @@ -8,12 +11,12 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-08-06 09:40-0400\n" +"PO-Revision-Date: 2017-08-08 08:32-0400\n" "Last-Translator: chang-ho,cha <changho.cha@gmail.com>\n" "Language-Team: Korean (https://translate.zanata.org/project/view/GitLab)\n" "Language: ko\n" +"Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Zanata 3.9.6\n" -"Plural-Forms: nplurals=1; plural=0\n" msgid "%d commit" msgid_plural "%d commits" @@ -25,7 +28,7 @@ msgid_plural "" msgstr[0] "%s 추가 커밋은 성능 이슈를 방지하기 위해 생략되었습니다." msgid "%{commit_author_link} committed %{commit_timeago}" -msgstr "%{commit_timeago} 에 %{commit_author_link} 이(가) 커밋하였습니다. " +msgstr "%{commit_timeago} 에 %{commit_author_link} 님이 커밋하였습니다. " msgid "1 pipeline" msgid_plural "%d pipelines" @@ -791,7 +794,7 @@ msgid "Select target branch" msgstr "대상 브랜치 선택" msgid "Set a password on your account to pull or push via %{protocol}." -msgstr "%{protocol}을(를) 통해 Pull 하거나 Push하려면 계정에 패스워드를 설정하십시오." +msgstr "%{protocol} 프로토콜을 통해 Pull 하거나 Push하려면 계정에 패스워드를 설정하십시오." msgid "Set up CI" msgstr "CI 설정" @@ -1118,21 +1121,11 @@ msgstr "이 단계를 보여주기에 충분한 데이터가 없습니다." msgid "Withdraw Access Request" msgstr "액세스 요청 철회" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "%{group_name}을(를) 제거하려고합니다.\n" -"\"정말로\" 확실합니까?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{group_name} 그룹을 제거하려고합니다. \"정말로\" 확실합니까?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"%{project_name_with_namespace}을(를) 삭제하려고합니다.\n" -"삭제된 프로젝트를 복원 할 수 없습니다!\n" -"\"정말로\" 확실합니까?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{project_name_with_namespace} 프로젝트를 삭제하려고합니다. "삭제된 프로젝트를 복원 할 수 없습니다! \"정말로\" 확실합니까?" msgid "" "You are going to remove the fork relationship to source project " @@ -1185,7 +1178,7 @@ msgid "" "You won't be able to pull or push project code via SSH until you " "%{add_ssh_key_link} to your profile" msgstr "" -"당신의 프로필에 %{add_ssh_key_link} 을(를) 하기 전에는 SSH를 통해 프로젝트 코드를 Pull 하거나 Push 할 수 " +"당신의 프로필에 %{add_ssh_key_link} 를 하기 전에는 SSH를 통해 프로젝트 코드를 Pull 하거나 Push 할 수 " "없습니다" msgid "Your name" diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po index 2eaadb64124..a2df8ea549c 100644 --- a/locale/pt_BR/gitlab.po +++ b/locale/pt_BR/gitlab.po @@ -1164,30 +1164,12 @@ msgstr "Esta etapa não possui dados suficientes para exibição." msgid "Withdraw Access Request" msgstr "Remover Requisição de Acesso" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Você vai remover %{group_name}.\n" -"Grupos removidos NÃO PODEM ser restaurados!\n" -"Você está ABSOLUTAMENTE certo?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Você irá remover %{project_name_with_namespace}.\n" -"O projeto removido NÃO PODE ser restaurado!\n" -"Tem certeza ABSOLUTA?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Você vai remover %{group_name}. Grupos removidos NÃO PODEM ser restaurados! Você está ABSOLUTAMENTE certo?" -msgid "" -"You are going to remove the fork relationship to source project " -"%{forked_from_project}. Are you ABSOLUTELY sure?" -msgstr "" -"Você ira remover o relacionamento de fork com o projeto original " -"%{forked_from_project}. Tem certeza ABSOLUTA?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Você irá remover %{project_name_with_namespace}. O projeto removido NÃO PODE ser restaurado! Tem certeza ABSOLUTA?" msgid "" "You are going to transfer %{project_name_with_namespace} to another owner. " diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po index 78f7b059077..6661232850a 100644 --- a/locale/ru/gitlab.po +++ b/locale/ru/gitlab.po @@ -1179,23 +1179,11 @@ msgstr "Информация по этапу отсутствует." msgid "Withdraw Access Request" msgstr "Отменить запрос доступа" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Вы собираетесь удалить %{group_name}.\n" -"Удаленные группы НЕ МОГУТ быть восстановлены!\n" -"Вы АБСОЛЮТНО уверены?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Вы собираетесь удалить %{group_name}. Удаленные группы НЕ МОГУТ быть восстановлены! Вы АБСОЛЮТНО уверены?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Вы хотите удалить %{project_name_with_namespace}.\n" -"Удаленный проект НЕ МОЖЕТ быть восстановлен!\n" -"Вы АБСОЛЮТНО уверены?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Вы хотите удалить %{project_name_with_namespace}. Удаленный проект НЕ МОЖЕТ быть восстановлен! Вы АБСОЛЮТНО уверены?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 78144d3755d..0ac0499e315 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -1173,23 +1173,11 @@ msgstr "Ми не маємо достатньо даних для показу msgid "Withdraw Access Request" msgstr "Скасувати запит доступу" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Ви хочете видалити %{group_name}.\n" -"Видалені групи НЕ МОЖНА буду відновити!\n" -"Ви АБСОЛЮТНО впевнені?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Ви хочете видалити %{group_name}. Видалені групи НЕ МОЖНА буду відновити! Ви АБСОЛЮТНО впевнені?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Ви хочете видалити %{project_name_with_namespace}.\n" -"Видалений проект НЕ МОЖЕ бути відновлений!\n" -"Ви АБСОЛЮТНО впевнені?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Ви хочете видалити %{project_name_with_namespace}. Видалений проект НЕ МОЖЕ бути відновлений! Ви АБСОЛЮТНО впевнені?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index 4a550db55d2..a3d0027212c 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -1101,18 +1101,10 @@ msgstr "该阶段的数据不足,无法显示。" msgid "Withdraw Access Request" msgstr "取消权限申请" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "即将删除 %{group_name}。\n" -"已删除的群组无法恢复!\n" -"确定继续吗?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "即将删除 %{group_name}。已删除的群组无法恢复!确定继续吗?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "即将要删除 %{project_name_with_namespace}。已删除的项目无法恢复!确定继续吗?" msgid "" diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 69b2bf80dbf..f4d33862a36 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -1100,18 +1100,10 @@ msgstr "該階段的數據不足,無法顯示。" msgid "Withdraw Access Request" msgstr "取消權限申请" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "即將刪除 %{group_name}。\n" -"已刪除的群組無法恢復!\n" -"確定繼續嗎?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "即將刪除 %{group_name}。已刪除的群組無法恢復!確定繼續嗎?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "即將要刪除 %{project_name_with_namespace}。已刪除的項目無法恢複!確定繼續嗎?" msgid "" diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 4fd728659c6..205d4712316 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -1111,18 +1111,10 @@ msgstr "因該階段的資料不足而無法顯示相關資訊" msgid "Withdraw Access Request" msgstr "取消權限申請" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "即將要刪除 %{group_name}。\n" -"被刪除的群組完全無法救回來喔!\n" -"真的「100%確定」要這麼做嗎?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "即將要刪除 %{group_name}。被刪除的群組完全無法救回來喔!真的「100%確定」要這麼做嗎?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "即將要刪除 %{project_name_with_namespace}。被刪除的專案完全無法救回來喔!真的「100%確定」要這麼做嗎?" msgid "" diff --git a/package.json b/package.json index c5247a63e67..cbb9be3a27f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "vue-loader": "^11.3.4", "vue-resource": "^1.3.4", "vue-template-compiler": "^2.2.6", - "webpack": "^3.4.0", + "webpack": "^3.5.4", "webpack-bundle-analyzer": "^2.8.2", "webpack-stats-plugin": "^0.1.5" }, diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb index 4f83a773645..12e56404cf6 100644 --- a/qa/qa/runtime/release.rb +++ b/qa/qa/runtime/release.rb @@ -21,7 +21,7 @@ module QA end def self.method_missing(name, *args) - self.new.strategy.public_send(name, *args) + self.new.strategy.public_send(name, *args) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/rubocop/cop/migration/safer_boolean_column.rb b/rubocop/cop/migration/safer_boolean_column.rb new file mode 100644 index 00000000000..0335c25d85d --- /dev/null +++ b/rubocop/cop/migration/safer_boolean_column.rb @@ -0,0 +1,94 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # This cop requires a default value and disallows nulls for boolean + # columns on small tables. + # + # In general, this prevents 3-state-booleans. + # https://robots.thoughtbot.com/avoid-the-threestate-boolean-problem + # + # In particular, for the `application_settings` table, this ensures that + # upgraded installations get a proper default for the new boolean setting. + # A developer might otherwise mistakenly assume that a value in + # `ApplicationSetting.defaults` is sufficient. + # + # See https://gitlab.com/gitlab-org/gitlab-ee/issues/2750 for more + # information. + class SaferBooleanColumn < RuboCop::Cop::Cop + include MigrationHelpers + + DEFAULT_OFFENSE = 'Boolean columns on the `%s` table should have a default. You may wish to use `add_column_with_default`.'.freeze + NULL_OFFENSE = 'Boolean columns on the `%s` table should disallow nulls.'.freeze + DEFAULT_AND_NULL_OFFENSE = 'Boolean columns on the `%s` table should have a default and should disallow nulls. You may wish to use `add_column_with_default`.'.freeze + + SMALL_TABLES = %i[ + application_settings + ].freeze + + def_node_matcher :add_column?, <<~PATTERN + (send nil :add_column $...) + PATTERN + + def on_send(node) + return unless in_migration?(node) + + matched = add_column?(node) + + return unless matched + + table, _, type = matched.to_a.take(3).map(&:children).map(&:first) + opts = matched[3] + + return unless SMALL_TABLES.include?(table) && type == :boolean + + no_default = no_default?(opts) + nulls_allowed = nulls_allowed?(opts) + + offense = if no_default && nulls_allowed + DEFAULT_AND_NULL_OFFENSE + elsif no_default + DEFAULT_OFFENSE + elsif nulls_allowed + NULL_OFFENSE + end + + add_offense(node, :expression, format(offense, table)) if offense + end + + def no_default?(opts) + return true unless opts + + each_hash_node_pair(opts) do |key, value| + return value == 'nil' if key == :default + end + end + + def nulls_allowed?(opts) + return true unless opts + + each_hash_node_pair(opts) do |key, value| + return value != 'false' if key == :null + end + end + + def each_hash_node_pair(hash_node, &block) + hash_node.each_node(:pair) do |pair| + key = hash_pair_key(pair) + value = hash_pair_value(pair) + yield(key, value) + end + end + + def hash_pair_key(pair) + pair.children[0].children[0] + end + + def hash_pair_value(pair) + pair.children[1].source + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 3fbd5b0163c..1b6e8991a17 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -13,6 +13,7 @@ require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' require_relative 'cop/migration/add_timestamps' require_relative 'cop/migration/datetime' +require_relative 'cop/migration/safer_boolean_column' require_relative 'cop/migration/hash_index' require_relative 'cop/migration/remove_concurrent_index' require_relative 'cop/migration/remove_index' diff --git a/scripts/detect-new-flaky-examples b/scripts/detect-new-flaky-examples new file mode 100755 index 00000000000..3bee4f9a34b --- /dev/null +++ b/scripts/detect-new-flaky-examples @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby + +require 'json' + +report_file = ARGV.shift +unless report_file + puts 'usage: detect-new-flaky-examples <report-file>' + exit 1 +end + +puts "Loading #{report_file}..." +report = JSON.parse(File.read(report_file)) + +if report.any? + puts "New flaky examples were detected!\n" + puts JSON.pretty_generate(report) + exit 1 +else + puts "No new flaky examples detected.\n" + exit 0 +end diff --git a/scripts/merge-reports b/scripts/merge-reports index aad76bcc327..3a421f1f1fc 100755 --- a/scripts/merge-reports +++ b/scripts/merge-reports @@ -4,7 +4,7 @@ require 'json' main_report_file = ARGV.shift unless main_report_file - puts 'usage: merge_reports <main-report> [extra reports...]' + puts 'usage: merge-reports <main-report> [extra reports...]' exit 1 end diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 91aff0db7cc..6d8b9865dcb 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -4,56 +4,90 @@ load File.expand_path('../../bin/changelog', __dir__) describe 'bin/changelog' do describe ChangelogOptionParser do - it 'parses --ammend' do - options = described_class.parse(%w[foo bar --amend]) + describe '.parse' do + it 'parses --amend' do + options = described_class.parse(%w[foo bar --amend]) - expect(options.amend).to eq true - end + expect(options.amend).to eq true + end - it 'parses --force and -f' do - %w[--force -f].each do |flag| - options = described_class.parse(%W[foo #{flag} bar]) + it 'parses --force and -f' do + %w[--force -f].each do |flag| + options = described_class.parse(%W[foo #{flag} bar]) - expect(options.force).to eq true + expect(options.force).to eq true + end end - end - it 'parses --merge-request and -m' do - %w[--merge-request -m].each do |flag| - options = described_class.parse(%W[foo #{flag} 1234 bar]) + it 'parses --merge-request and -m' do + %w[--merge-request -m].each do |flag| + options = described_class.parse(%W[foo #{flag} 1234 bar]) - expect(options.merge_request).to eq 1234 + expect(options.merge_request).to eq 1234 + end end - end - it 'parses --dry-run and -n' do - %w[--dry-run -n].each do |flag| - options = described_class.parse(%W[foo #{flag} bar]) + it 'parses --dry-run and -n' do + %w[--dry-run -n].each do |flag| + options = described_class.parse(%W[foo #{flag} bar]) - expect(options.dry_run).to eq true + expect(options.dry_run).to eq true + end end - end - it 'parses --git-username and -u' do - allow(described_class).to receive(:git_user_name).and_return('Jane Doe') + it 'parses --git-username and -u' do + allow(described_class).to receive(:git_user_name).and_return('Jane Doe') - %w[--git-username -u].each do |flag| - options = described_class.parse(%W[foo #{flag} bar]) + %w[--git-username -u].each do |flag| + options = described_class.parse(%W[foo #{flag} bar]) - expect(options.author).to eq 'Jane Doe' + expect(options.author).to eq 'Jane Doe' + end + end + + it 'parses --type and -t' do + %w[--type -t].each do |flag| + options = described_class.parse(%W[foo #{flag} security]) + + expect(options.type).to eq 'security' + end end - end - it 'parses -h' do - expect do - expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout - end.to raise_error(SystemExit) + it 'parses -h' do + expect do + expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout + end.to raise_error(SystemExit) + end + + it 'assigns title' do + options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend]) + + expect(options.title).to eq 'foo bar baz' + end end - it 'assigns title' do - options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend]) + describe '.read_type' do + let(:type) { '1' } - expect(options.title).to eq 'foo bar baz' + it 'reads type from $stdin' do + expect($stdin).to receive(:getc).and_return(type) + expect do + expect(described_class.read_type).to eq('added') + end.to output.to_stdout + end + + context 'invalid type given' do + let(:type) { '99' } + + it 'shows error message and exits the program' do + allow($stdin).to receive(:getc).and_return(type) + expect do + expect do + expect { described_class.read_type }.to raise_error(SystemExit) + end.to output("Invalid category index, please select an index between 1 and 7\n").to_stderr + end.to output.to_stdout + end + end end end end diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index a31e44fa928..74634dac713 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -20,12 +20,12 @@ describe 'mail_room.yml' do YAML.load(output) end - before(:each) do + before do stub_env('GITLAB_REDIS_QUEUES_CONFIG_FILE', absolute_path(queues_config_path)) clear_queues_raw_config end - after(:each) do + after do clear_queues_raw_config end diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 65587064eb1..373260b3978 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -12,12 +12,24 @@ describe Admin::ProjectsController do it 'retrieves the project for the given visibility level' do get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC] + expect(response.body).to match(project.name) end it 'does not retrieve the project' do get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL] + expect(response.body).not_to match(project.name) end + + it 'does not respond with projects pending deletion' do + pending_delete_project = create(:project, pending_delete: true) + + get :index + + expect(response).to have_http_status(200) + expect(response.body).not_to match(pending_delete_project.name) + expect(response.body).to match(project.name) + end end end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 29c449d6aa9..3d21b695af4 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -127,7 +127,7 @@ describe Admin::UsersController do describe 'POST create' do it 'creates the user' do - expect{ post :create, user: attributes_for(:user) }.to change{ User.count }.by(1) + expect { post :create, user: attributes_for(:user) }.to change { User.count }.by(1) end it 'shows only one error message for an invalid email' do diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 3c396e36b24..2fbab1e4040 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe AutocompleteController do - let!(:project) { create(:project) } - let!(:user) { create(:user) } + let(:project) { create(:project) } + let(:user) { project.owner } context 'GET users' do let!(:user2) { create(:user) } @@ -11,7 +11,6 @@ describe AutocompleteController do context 'project members' do before do sign_in(user) - project.add_master(user) end describe 'GET #users with project ID' do @@ -19,11 +18,11 @@ describe AutocompleteController do get(:users, project_id: project.id) end - let(:body) { JSON.parse(response.body) } - - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 2 } - it { expect(body.map { |u| u["username"] }).to include(user.username) } + it 'returns the project members' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(1) + expect(json_response.map { |u| u["username"] }).to include(user.username) + end end describe 'GET #users with unknown project' do @@ -39,20 +38,20 @@ describe AutocompleteController do let(:group) { create(:group) } before do - sign_in(user) group.add_owner(user) + sign_in(user) end - let(:body) { JSON.parse(response.body) } - describe 'GET #users with group ID' do before do get(:users, group_id: group.id) end - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 1 } - it { expect(body.first["username"]).to eq user.username } + it 'returns the group members' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(1) + expect(json_response.first["username"]).to eq user.username + end end describe 'GET #users with unknown group ID' do @@ -65,23 +64,22 @@ describe AutocompleteController do end context 'non-member login for public project' do - let!(:project) { create(:project, :public) } + let(:project) { create(:project, :public) } before do sign_in(non_member) - project.add_master(user) end - let(:body) { JSON.parse(response.body) } - describe 'GET #users with project ID' do before do get(:users, project_id: project.id, current_user: true) end - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 3 } - it { expect(body.map { |u| u['username'] }).to include(user.username, non_member.username) } + it 'returns the project members and non-members' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['username'] }).to include(user.username, non_member.username) + end end end @@ -91,10 +89,8 @@ describe AutocompleteController do get(:users) end - let(:body) { JSON.parse(response.body) } - - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq User.count } + it { expect(json_response).to be_kind_of(Array) } + it { expect(json_response.size).to eq User.count } end context 'user order' do @@ -106,7 +102,7 @@ describe AutocompleteController do sign_in(user) get(:users, search: 'user') - response_usernames = JSON.parse(response.body).map { |user| user['username'] } + response_usernames = json_response.map { |user| user['username'] } expect(response_usernames.take(3)).to match_array([user.username, reported_user.username, user1.username]) end @@ -120,15 +116,12 @@ describe AutocompleteController do get(:users, per_page: per_page) end - let(:body) { JSON.parse(response.body) } - - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq per_page } + it { expect(json_response).to be_kind_of(Array) } + it { expect(json_response.size).to eq(per_page) } end context 'unauthenticated user' do let(:public_project) { create(:project, :public) } - let(:body) { JSON.parse(response.body) } describe 'GET #users with public project' do before do @@ -136,8 +129,8 @@ describe AutocompleteController do get(:users, project_id: public_project.id) end - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 2 } + it { expect(json_response).to be_kind_of(Array) } + it { expect(json_response.size).to eq 2 } end describe 'GET #users with project' do @@ -170,8 +163,8 @@ describe AutocompleteController do get(:users) end - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 0 } + it { expect(json_response).to be_kind_of(Array) } + it { expect(json_response).to be_empty } end describe 'GET #users with todo filter' do @@ -179,14 +172,12 @@ describe AutocompleteController do get :users, todo_filter: true expect(response.status).to eq 200 - expect(body).to be_kind_of(Array) + expect(json_response).to be_kind_of(Array) end end end context 'author of issuable included' do - let(:body) { JSON.parse(response.body) } - context 'authenticated' do before do sign_in(user) @@ -195,13 +186,13 @@ describe AutocompleteController do it 'includes the author' do get(:users, author_id: non_member.id) - expect(body.first["username"]).to eq non_member.username + expect(json_response.first["username"]).to eq non_member.username end it 'rejects non existent user ids' do get(:users, author_id: 99999) - expect(body.collect { |u| u['id'] }).not_to include(99999) + expect(json_response.collect { |u| u['id'] }).not_to include(99999) end end @@ -209,7 +200,7 @@ describe AutocompleteController do it 'returns empty result' do get(:users, author_id: non_member.id) - expect(body).to be_empty + expect(json_response).to be_empty end end end @@ -222,10 +213,9 @@ describe AutocompleteController do it 'skips the user IDs passed' do get(:users, skip_users: [user, user2].map(&:id)) - other_user_ids = [non_member, project.owner, project.creator].map(&:id) - response_user_ids = JSON.parse(response.body).map { |user| user['id'] } + response_user_ids = json_response.map { |user| user['id'] } - expect(response_user_ids).to contain_exactly(*other_user_ids) + expect(response_user_ids).to contain_exactly(non_member.id) end end end @@ -249,17 +239,15 @@ describe AutocompleteController do get(:projects, project_id: project.id) end - let(:body) { JSON.parse(response.body) } - - it do - expect(body).to be_kind_of(Array) - expect(body.size).to eq 2 + it 'returns projects' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(2) - expect(body.first['id']).to eq 0 - expect(body.first['name_with_namespace']).to eq 'No project' + expect(json_response.first['id']).to eq(0) + expect(json_response.first['name_with_namespace']).to eq 'No project' - expect(body.last['id']).to eq authorized_project.id - expect(body.last['name_with_namespace']).to eq authorized_project.name_with_namespace + expect(json_response.last['id']).to eq authorized_project.id + expect(json_response.last['name_with_namespace']).to eq authorized_project.name_with_namespace end end end @@ -275,14 +263,12 @@ describe AutocompleteController do get(:projects, project_id: project.id, search: 'rugged') end - let(:body) { JSON.parse(response.body) } + it 'returns projects' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(2) - it do - expect(body).to be_kind_of(Array) - expect(body.size).to eq 2 - - expect(body.last['id']).to eq authorized_search_project.id - expect(body.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace + expect(json_response.last['id']).to eq authorized_search_project.id + expect(json_response.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace end end end @@ -304,11 +290,9 @@ describe AutocompleteController do get(:projects, project_id: project.id) end - let(:body) { JSON.parse(response.body) } - - it do - expect(body).to be_kind_of(Array) - expect(body.size).to eq 3 # Of a total of 4 + it 'returns projects' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq 3 # Of a total of 4 end end end @@ -328,17 +312,15 @@ describe AutocompleteController do get(:projects, project_id: project.id, offset_id: authorized_project.id) end - let(:body) { JSON.parse(response.body) } - - it do - expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there - expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either + it 'returns "No project"' do + expect(json_response.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there + expect(json_response.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either end end end context 'authorized projects without admin_issue ability' do - before(:each) do + before do authorized_project.add_guest(user) expect(user.can?(:admin_issue, authorized_project)).to eq(false) @@ -349,13 +331,10 @@ describe AutocompleteController do get(:projects, project_id: project.id) end - let(:body) { JSON.parse(response.body) } - - it do - expect(body).to be_kind_of(Array) - expect(body.size).to eq 1 # 'No project' - - expect(body.first['id']).to eq 0 + it 'returns a single "No project"' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(1) # 'No project' + expect(json_response.first['id']).to eq 0 end end end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index e478a253b3f..e00403118a0 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -24,7 +24,7 @@ describe InvitesController do describe 'GET #decline' do it 'declines user' do get :decline, id: token - expect{member.reload}.to raise_error ActiveRecord::RecordNotFound + expect {member.reload}.to raise_error ActiveRecord::RecordNotFound expect(response).to have_http_status(302) expect(flash[:notice]).to include 'You have declined the invitation to join' diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index bdee3894a13..b571b11dcac 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1,7 +1,7 @@ require('spec_helper') describe Projects::IssuesController do - let(:project) { create(:project_empty_repo) } + let(:project) { create(:project) } let(:user) { create(:user) } let(:issue) { create(:issue, project: project) } @@ -292,13 +292,13 @@ describe Projects::IssuesController do it 'rejects an issue recognized as a spam' do expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) - expect { update_spam_issue }.not_to change{ issue.reload.title } + expect { update_spam_issue }.not_to change { issue.reload.title } end it 'rejects an issue recognized as a spam when recaptcha disabled' do stub_application_setting(recaptcha_enabled: false) - expect { update_spam_issue }.not_to change{ issue.reload.title } + expect { update_spam_issue }.not_to change { issue.reload.title } end it 'creates a spam log' do @@ -358,7 +358,7 @@ describe Projects::IssuesController do end it 'accepts an issue after recaptcha is verified' do - expect{ update_verified_issue }.to change{ issue.reload.title }.to(spammy_title) + expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title) end it 'marks spam log as recaptcha_verified' do @@ -841,7 +841,7 @@ describe Projects::IssuesController do describe 'POST #toggle_award_emoji' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it "toggles the award emoji" do @@ -855,6 +855,8 @@ describe Projects::IssuesController do end describe 'POST create_merge_request' do + let(:project) { create(:project, :repository) } + before do project.add_developer(user) sign_in(user) diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb index 974330e2bbd..41d211ed1bb 100644 --- a/spec/controllers/projects/todos_controller_spec.rb +++ b/spec/controllers/projects/todos_controller_spec.rb @@ -67,7 +67,7 @@ describe Projects::TodosController do end it "doesn't create todo" do - expect{ go }.not_to change { user.todos.count } + expect { go }.not_to change { user.todos.count } expect(response).to have_http_status(404) end end @@ -135,7 +135,7 @@ describe Projects::TodosController do end it "doesn't create todo" do - expect{ go }.not_to change { user.todos.count } + expect { go }.not_to change { user.todos.count } expect(response).to have_http_status(404) end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 8ecd8b6ca71..c0e48046937 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -578,6 +578,118 @@ describe ProjectsController do end end + describe '#export' do + before do + sign_in(user) + + project.add_master(user) + end + + context 'when project export is enabled' do + it 'returns 302' do + get :export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(302) + end + end + + context 'when project export is disabled' do + before do + stub_application_setting(project_export_enabled?: false) + end + + it 'returns 404' do + get :export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(404) + end + end + end + + describe '#download_export' do + before do + sign_in(user) + + project.add_master(user) + end + + context 'when project export is enabled' do + it 'returns 302' do + get :download_export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(302) + end + end + + context 'when project export is disabled' do + before do + stub_application_setting(project_export_enabled?: false) + end + + it 'returns 404' do + get :download_export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(404) + end + end + end + + describe '#remove_export' do + before do + sign_in(user) + + project.add_master(user) + end + + context 'when project export is enabled' do + it 'returns 302' do + post :remove_export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(302) + end + end + + context 'when project export is disabled' do + before do + stub_application_setting(project_export_enabled?: false) + end + + it 'returns 404' do + post :remove_export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(404) + end + end + end + + describe '#generate_new_export' do + before do + sign_in(user) + + project.add_master(user) + end + + context 'when project export is enabled' do + it 'returns 302' do + post :generate_new_export, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(302) + end + end + + context 'when project export is disabled' do + before do + stub_application_setting(project_export_enabled?: false) + end + + it 'returns 404' do + post :generate_new_export, namespace_id: project.namespace, id: project + + 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 diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 634563fc290..5a4ab39ab86 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -5,7 +5,7 @@ describe RegistrationsController do let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } } context 'email confirmation' do - around(:each) do |example| + around do |example| perform_enqueued_jobs do example.run end @@ -15,7 +15,7 @@ describe RegistrationsController do it 'signs the user in' do allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) - expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size } + expect { post(:create, user_params) }.not_to change { ActionMailer::Base.deliveries.size } expect(subject.current_user).not_to be_nil end end diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb index 1c494b8c7ab..225753333ee 100644 --- a/spec/controllers/snippets/notes_controller_spec.rb +++ b/spec/controllers/snippets/notes_controller_spec.rb @@ -138,7 +138,7 @@ describe Snippets::NotesController do end it "deletes the note" do - expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0) + expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0) end context 'system note' do @@ -147,7 +147,7 @@ describe Snippets::NotesController do end it "does not delete the note" do - expect{ delete :destroy, request_params }.not_to change{ Note.count } + expect { delete :destroy, request_params }.not_to change { Note.count } end end end @@ -166,7 +166,7 @@ describe Snippets::NotesController do end it "does not update the note" do - expect{ delete :destroy, request_params }.not_to change{ Note.count } + expect { delete :destroy, request_params }.not_to change { Note.count } end end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 475ceda11fe..7c5d059760f 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -186,8 +186,8 @@ describe SnippetsController do end context 'when the snippet description contains a file' do - let(:picture_file) { '/system/temp/secret56/picture.jpg' } - let(:text_file) { '/system/temp/secret78/text.txt' } + let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } + let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:description) do "Description with picture: ![picture](/uploads#{picture_file}) and "\ "text: [text.txt](/uploads#{text_file})" @@ -208,8 +208,8 @@ describe SnippetsController do snippet = subject expected_description = "Description with picture: "\ - "![picture](/uploads/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ - "text: [text.txt](/uploads/system/personal_snippet/#{snippet.id}/secret78/text.txt)" + "![picture](/uploads/-/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ + "text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/secret78/text.txt)" expect(snippet.description).to eq(expected_description) end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index b3a40f5d15c..b29f3d861be 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -102,7 +102,7 @@ describe UploadsController do subject expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"/uploads/system/temp" + expect(response.body).to match "\"url\":\"/uploads/-/system/temp" end it 'does not create an Upload record' do @@ -119,7 +119,7 @@ describe UploadsController do subject expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"/uploads/system/temp" + expect(response.body).to match "\"url\":\"/uploads/-/system/temp" end it 'does not create an Upload record' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index a64ad73cba8..2cecd2646fc 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -92,8 +92,14 @@ describe UsersController do before do sign_in(user) project.team << [user, :developer] - EventCreateService.new.push(project, user, []) - EventCreateService.new.push(forked_project, user, []) + + push_data = Gitlab::DataBuilder::Push.build_sample(project, user) + + fork_push_data = Gitlab::DataBuilder::Push + .build_sample(forked_project, user) + + EventCreateService.new.push(project, user, push_data) + EventCreateService.new.push(forked_project, user, fork_push_data) end it 'includes forked projects' do diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 29ad1af9fd9..e5abfd67d60 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -10,6 +10,10 @@ FactoryGirl.define do after(:build) do |deployment, evaluator| deployment.project ||= deployment.environment.project + + unless deployment.project.repository_exists? + allow(deployment.project.repository).to receive(:fetch_ref) + end end end end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index 11d2016955c..ad9f7e2caef 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -2,6 +2,7 @@ FactoryGirl.define do factory :event do project author factory: :user + action Event::JOINED trait(:created) { action Event::CREATED } trait(:updated) { action Event::UPDATED } @@ -20,4 +21,19 @@ FactoryGirl.define do target factory: :closed_issue end end + + factory :push_event, class: PushEvent do + project factory: :project_empty_repo + author factory: :user + action Event::PUSHED + end + + factory :push_event_payload do + event + commit_count 1 + action :pushed + ref_type :branch + ref 'master' + commit_to '3cdce97ed87c91368561584e7358f4d46e3e173c' + end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 1bc530d06db..cbec716d6ea 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -68,6 +68,17 @@ FactoryGirl.define do merge_user author end + after(:build) do |merge_request| + target_project = merge_request.target_project + source_project = merge_request.source_project + + # Fake `write_ref` if we don't have repository + # We have too many existing tests replying on this behaviour + unless [target_project, source_project].all?(&:repository_exists?) + allow(merge_request).to receive(:write_ref) + end + end + factory :merged_merge_request, traits: [:merged] factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:opened] diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 3f8e7030b1c..4a2034b31b3 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -8,12 +8,47 @@ FactoryGirl.define do factory :project, class: 'Project' do sequence(:name) { |n| "project#{n}" } path { name.downcase.gsub(/\s/, '_') } - namespace - creator - # Behaves differently to nil due to cache_has_external_issue_tracker has_external_issue_tracker false + # Associations + namespace + creator { group ? create(:user) : namespace&.owner } + + # Nest Project Feature attributes + transient do + wiki_access_level ProjectFeature::ENABLED + builds_access_level ProjectFeature::ENABLED + snippets_access_level ProjectFeature::ENABLED + issues_access_level ProjectFeature::ENABLED + merge_requests_access_level ProjectFeature::ENABLED + repository_access_level ProjectFeature::ENABLED + end + + after(:create) do |project, evaluator| + # Builds and MRs can't have higher visibility level than repository access level. + builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min + merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min + + project.project_feature.update_columns( + wiki_access_level: evaluator.wiki_access_level, + builds_access_level: builds_access_level, + snippets_access_level: evaluator.snippets_access_level, + issues_access_level: evaluator.issues_access_level, + merge_requests_access_level: merge_requests_access_level, + repository_access_level: evaluator.repository_access_level) + + # Normally the class Projects::CreateService is used for creating + # projects, and this class takes care of making sure the owner and current + # user have access to the project. Our specs don't use said service class, + # thus we must manually refresh things here. + unless project.group || project.pending_delete + project.add_master(project.owner) + end + + project.group&.refresh_members_authorized_projects + end + trait :public do visibility_level Gitlab::VisibilityLevel::PUBLIC end @@ -67,30 +102,28 @@ FactoryGirl.define do test_repo transient do - create_template nil + create_templates nil end after :create do |project, evaluator| - if evaluator.create_template - args = evaluator.create_template - - project.add_user(args[:user], args[:access]) + if evaluator.create_templates + templates_path = "#{evaluator.create_templates}_templates" project.repository.create_file( - args[:user], - ".gitlab/#{args[:path]}/bug.md", + project.creator, + ".gitlab/#{templates_path}/bug.md", 'something valid', message: 'test 3', branch_name: 'master') project.repository.create_file( - args[:user], - ".gitlab/#{args[:path]}/template_test.md", + project.creator, + ".gitlab/#{templates_path}/template_test.md", 'template_test', message: 'test 1', branch_name: 'master') project.repository.create_file( - args[:user], - ".gitlab/#{args[:path]}/feature_proposal.md", + project.creator, + ".gitlab/#{templates_path}/feature_proposal.md", 'feature_proposal', message: 'test 2', branch_name: 'master') @@ -142,44 +175,6 @@ FactoryGirl.define do trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } - - # Nest Project Feature attributes - transient do - wiki_access_level ProjectFeature::ENABLED - builds_access_level ProjectFeature::ENABLED - snippets_access_level ProjectFeature::ENABLED - issues_access_level ProjectFeature::ENABLED - merge_requests_access_level ProjectFeature::ENABLED - repository_access_level ProjectFeature::ENABLED - end - - after(:create) do |project, evaluator| - # Builds and MRs can't have higher visibility level than repository access level. - builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min - merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min - - project.project_feature - .update_attributes!( - wiki_access_level: evaluator.wiki_access_level, - builds_access_level: builds_access_level, - snippets_access_level: evaluator.snippets_access_level, - issues_access_level: evaluator.issues_access_level, - merge_requests_access_level: merge_requests_access_level, - repository_access_level: evaluator.repository_access_level - ) - - # Normally the class Projects::CreateService is used for creating - # projects, and this class takes care of making sure the owner and current - # user have access to the project. Our specs don't use said service class, - # thus we must manually refresh things here. - owner = project.owner - - if owner && owner.is_a?(User) && !project.pending_delete - project.members.create!(user: owner, access_level: Gitlab::Access::MASTER) - end - - project.group&.refresh_members_authorized_projects - end end # Project with empty repository diff --git a/spec/factories/users.rb b/spec/factories/users.rb index e60fe713bc3..4000cd085b7 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,5 +1,5 @@ FactoryGirl.define do - factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do + factory :user, aliases: [:author, :assignee, :recipient, :owner, :resource_owner] do email { generate(:email) } name { generate(:name) } username { generate(:username) } @@ -8,6 +8,10 @@ FactoryGirl.define do confirmation_token { nil } can_create_group true + after(:stub) do |user| + user.notification_email = user.email + end + before(:create) do |user| user.ensure_rss_token end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index c9591a7d854..dbb0ae9c86e 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -35,6 +35,7 @@ feature 'Admin updates settings' do fill_in 'Help page text', with: 'Example text' check 'Hide marketing-related entries from help' fill_in 'Support page URL', with: 'http://example.com/help' + uncheck 'Project export enabled' click_button 'Save' expect(current_application_settings.gravatar_enabled).to be_falsey @@ -42,6 +43,7 @@ feature 'Admin updates settings' do expect(current_application_settings.help_page_text).to eq "Example text" expect(current_application_settings.help_page_hide_commercial_content).to be_truthy expect(current_application_settings.help_page_support_url).to eq "http://example.com/help" + expect(current_application_settings.project_export_enabled).to be_falsey expect(page).to have_content "Application settings saved successfully" end @@ -72,7 +74,7 @@ feature 'Admin updates settings' do context 'sign-in restrictions', :js do it 'de-activates oauth sign-in source' do find('.btn', text: 'GitLab.com').click - + expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active') end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 8d3d4ff8773..c3bf50ef9d1 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -15,10 +15,12 @@ describe 'Issue Boards', js: true do let!(:list) { create(:list, board: board, label: development, position: 0) } let(:card) { find('.board:nth-child(2)').first('.card') } - before do - Timecop.freeze + around do |example| + Timecop.freeze { example.run } + end - project.team << [user, :master] + before do + project.add_master(user) sign_in(user) @@ -26,10 +28,6 @@ describe 'Issue Boards', js: true do wait_for_requests end - after do - Timecop.return - end - it 'shows sidebar when clicking issue' do click_card(card) diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 64fbc80cb81..9a597a2d690 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -42,14 +42,14 @@ feature 'Contributions Calendar', :js do end def push_code_contribution - push_params = { - project: contributed_project, - action: Event::PUSHED, - author_id: user.id, - data: { commit_count: 3 } - } - - Event.create(push_params) + event = create(:push_event, project: contributed_project, author: user) + + create(:push_event_payload, + event: event, + commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce', + commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + commit_count: 3, + ref: 'master') end def note_comment_contribution diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 5c60cca10b9..bfe9dac3bd4 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -24,6 +24,12 @@ feature 'Cycle Analytics', js: true do expect(page).to have_content('Introducing Cycle Analytics') end + it 'shows pipeline summary' do + expect(new_issues_counter).to have_content('-') + expect(commits_counter).to have_content('-') + expect(deploys_counter).to have_content('-') + end + it 'shows active stage with empty message' do expect(page).to have_selector('.stage-nav-item.active', text: 'Issue') expect(page).to have_content("We don't have enough data to show this stage.") @@ -42,6 +48,12 @@ feature 'Cycle Analytics', js: true do visit project_cycle_analytics_path(project) end + it 'shows pipeline summary' do + expect(new_issues_counter).to have_content('1') + expect(commits_counter).to have_content('2') + expect(deploys_counter).to have_content('1') + end + it 'shows data on each stage' do expect_issue_to_be_present @@ -63,6 +75,20 @@ feature 'Cycle Analytics', js: true do click_stage('Production') expect_issue_to_be_present end + + context "when I change the time period observed" do + before do + _two_weeks_old_issue = create(:issue, project: project, created_at: 2.weeks.ago) + + click_button('Last 30 days') + click_link('Last 7 days') + wait_for_requests + end + + it 'shows only relevant data' do + expect(new_issues_counter).to have_content('1') + end + end end context "when my preferred language is Spanish" do @@ -109,6 +135,18 @@ feature 'Cycle Analytics', js: true do end end + def new_issues_counter + find(:xpath, "//p[contains(text(),'New Issue')]/preceding-sibling::h3") + end + + def commits_counter + find(:xpath, "//p[contains(text(),'Commits')]/preceding-sibling::h3") + end + + def deploys_counter + find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3") + end + def expect_issue_to_be_present expect(find('.stage-events')).to have_content(issue.title) expect(find('.stage-events')).to have_content(issue.author.name) diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb index 4917dfcf1d1..582868bac1e 100644 --- a/spec/features/dashboard/activity_spec.rb +++ b/spec/features/dashboard/activity_spec.rb @@ -23,27 +23,19 @@ feature 'Dashboard > Activity' do create(:merge_request, author: user, source_project: project, target_project: project) end - let(:push_event_data) do - { - before: Gitlab::Git::BLANK_SHA, - after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e', - ref: 'refs/heads/new_design', - user_id: user.id, - user_name: user.name, - repository: { - name: project.name, - url: 'localhost/rubinius', - description: '', - homepage: 'localhost/rubinius', - private: true - } - } - end - let(:note) { create(:note, project: project, noteable: merge_request) } let!(:push_event) do - create(:event, :pushed, data: push_event_data, project: project, author: user) + event = create(:push_event, project: project, author: user) + + create(:push_event_payload, + event: event, + action: :created, + commit_to: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e', + ref: 'new_design', + commit_count: 1) + + event end let!(:merged_event) do diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index acb21eab03f..d0316cfb18d 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -57,7 +57,7 @@ feature 'Edit group settings' do TestEnv.clean_test_path end - after(:example) do + after do TestEnv.clean_test_path end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 574bbe0e0e1..56144d17d4f 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -5,14 +5,12 @@ feature 'Group milestones', :js do let!(:project) { create(:project_empty_repo, group: group) } let(:user) { create(:group_member, :master, user: create(:user), group: group ).user } - before do - Timecop.freeze - - sign_in(user) + around do |example| + Timecop.freeze { example.run } end - after do - Timecop.return + before do + sign_in(user) end context 'create a milestone' do @@ -37,12 +35,12 @@ feature 'Group milestones', :js do context 'milestones list' do let!(:other_project) { create(:project_empty_repo, group: group) } - let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') } let!(:active_project_milestone1) { create(:milestone, project: project, state: 'active', title: 'v1.0') } let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') } - let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') } let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') } let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') } + let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') } + let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') } before do visit group_milestones_path(group) @@ -60,5 +58,30 @@ feature 'Group milestones', :js do expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1) expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1) end + + it 'updates milestone' do + page.within(".milestones #milestone_#{active_group_milestone.id}") do + click_link('Edit') + end + + page.within('.milestone-form') do + fill_in 'milestone_title', with: 'new title' + click_button('Update milestone') + end + + expect(find('#content-body h2')).to have_content('new title') + end + + it 'shows milestone detail and supports its edit' do + page.within(".milestones #milestone_#{active_group_milestone.id}") do + click_link(active_group_milestone.title) + end + + page.within('.detail-page-header') do + click_link('Edit') + end + + expect(page).to have_selector('.milestone-form') + end end end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index bd4f233cba9..93be3b066ee 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -50,7 +50,7 @@ describe 'Help Pages' do it 'hides the version check image if the image request fails' do # We use '--load-images=yes' with poltergeist so the image fails to load - expect(find('.js-version-status-badge', visible: false)).not_to be_visible + expect(page).to have_selector('.js-version-status-badge', visible: false) end end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 847e3856ba5..b2229b44f99 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -353,7 +353,7 @@ feature 'Issues > Labels bulk assignment' do context 'cannot bulk assign labels' do it do - expect(page).not_to have_button 'Edit Issues' + expect(page).not_to have_button 'Edit issues' expect(page).not_to have_css '.check-all-issues' expect(page).not_to have_css '.issue-check' end @@ -411,7 +411,7 @@ feature 'Issues > Labels bulk assignment' do def enable_bulk_update visit project_issues_path(project) - click_button 'Edit Issues' + click_button 'Edit issues' end def disable_bulk_update diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index a69bd8a09b7..2cc027aac9e 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -134,8 +134,10 @@ describe 'Dropdown assignee', :js do it 'fills in the assignee username when the assignee has not been filtered' do click_assignee(user_jacob.name) + wait_for_requests + expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }]) + expect_tokens([assignee_token(user_jacob.name)]) expect_filtered_search_input_empty end @@ -143,8 +145,10 @@ describe 'Dropdown assignee', :js do filtered_search.send_keys('roo') click_assignee(user.name) + wait_for_requests + expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end @@ -152,7 +156,7 @@ describe 'Dropdown assignee', :js do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: 'none' }]) + expect_tokens([assignee_token('none')]) expect_filtered_search_input_empty end end @@ -171,7 +175,7 @@ describe 'Dropdown assignee', :js do find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: user.username }]) + expect_tokens([assignee_token(user.username)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 4bbf18e1dbe..975dc035f2d 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -121,16 +121,20 @@ describe 'Dropdown author', js: true do it 'fills in the author username when the author has not been filtered' do click_author(user_jacob.name) + wait_for_requests + expect(page).to have_css(js_dropdown_author, visible: false) - expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }]) + expect_tokens([author_token(user_jacob.name)]) expect_filtered_search_input_empty end it 'fills in the author username when the author has been filtered' do click_author(user.name) + wait_for_requests + expect(page).to have_css(js_dropdown_author, visible: false) - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end end @@ -149,7 +153,7 @@ describe 'Dropdown author', js: true do find('#js-dropdown-author .filter-dropdown-item', text: user.username).click expect(page).to have_css(js_dropdown_author, visible: false) - expect_tokens([{ name: 'author', value: user.username }]) + expect_tokens([author_token(user.username)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 67eb0ef0119..e84b07ec2ef 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -47,7 +47,7 @@ describe 'Dropdown label', js: true do filtered_search.native.send_keys(:down, :down, :enter) - expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_tokens([label_token(bug_label.title)]) expect_filtered_search_input_empty end end @@ -178,7 +178,7 @@ describe 'Dropdown label', js: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_tokens([label_token(bug_label.title)]) expect_filtered_search_input_empty end @@ -187,7 +187,7 @@ describe 'Dropdown label', js: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_tokens([label_token(bug_label.title)]) expect_filtered_search_input_empty end @@ -195,7 +195,7 @@ describe 'Dropdown label', js: true do click_label(two_words_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }]) + expect_tokens([label_token("\"#{two_words_label.title}\"")]) expect_filtered_search_input_empty end @@ -203,7 +203,7 @@ describe 'Dropdown label', js: true do click_label(long_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }]) + expect_tokens([label_token("\"#{long_label.title}\"")]) expect_filtered_search_input_empty end @@ -211,7 +211,7 @@ describe 'Dropdown label', js: true do click_label(wont_fix_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }]) + expect_tokens([label_token("'#{wont_fix_label.title}'")]) expect_filtered_search_input_empty end @@ -219,7 +219,7 @@ describe 'Dropdown label', js: true do click_label(uppercase_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }]) + expect_tokens([label_token(uppercase_label.title)]) expect_filtered_search_input_empty end @@ -227,7 +227,7 @@ describe 'Dropdown label', js: true do click_label(special_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{special_label.title}" }]) + expect_tokens([label_token(special_label.title)]) expect_filtered_search_input_empty end @@ -235,7 +235,7 @@ describe 'Dropdown label', js: true do find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: 'none' }]) + expect_tokens([label_token('none', false)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 456eb05f241..5f99921ae2e 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -134,7 +134,7 @@ describe 'Dropdown milestone', :js do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_tokens([milestone_token(milestone.title)]) expect_filtered_search_input_empty end @@ -143,7 +143,7 @@ describe 'Dropdown milestone', :js do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_tokens([milestone_token(milestone.title)]) expect_filtered_search_input_empty end @@ -151,7 +151,7 @@ describe 'Dropdown milestone', :js do click_milestone(two_words_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }]) + expect_tokens([milestone_token("\"#{two_words_milestone.title}\"")]) expect_filtered_search_input_empty end @@ -159,7 +159,7 @@ describe 'Dropdown milestone', :js do click_milestone(long_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }]) + expect_tokens([milestone_token("\"#{long_milestone.title}\"")]) expect_filtered_search_input_empty end @@ -167,7 +167,7 @@ describe 'Dropdown milestone', :js do click_milestone(wont_fix_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }]) + expect_tokens([milestone_token("'#{wont_fix_milestone.title}'")]) expect_filtered_search_input_empty end @@ -175,7 +175,7 @@ describe 'Dropdown milestone', :js do click_milestone(uppercase_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }]) + expect_tokens([milestone_token(uppercase_milestone.title)]) expect_filtered_search_input_empty end @@ -183,7 +183,7 @@ describe 'Dropdown milestone', :js do click_milestone(special_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }]) + expect_tokens([milestone_token(special_milestone.title)]) expect_filtered_search_input_empty end @@ -191,7 +191,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('No Milestone') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_tokens([milestone_token('none', false)]) expect_filtered_search_input_empty end @@ -199,7 +199,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: 'upcoming' }]) + expect_tokens([milestone_token('upcoming', false)]) expect_filtered_search_input_empty end @@ -207,7 +207,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Started') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: 'started' }]) + expect_tokens([milestone_token('started', false)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index cd2cbf4bfe7..2070043d842 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -97,7 +97,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") - expect_tokens([{ name: 'author', value: user.username }]) + wait_for_requests + + expect_tokens([author_token(user.name)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -117,7 +119,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched author and text' do input_filtered_search("author:@#{user.username} #{search_term}") - expect_tokens([{ name: 'author', value: user.username }]) + wait_for_requests + + expect_tokens([author_token(user.name)]) expect_issues_list_count(3) expect_filtered_search_input(search_term) end @@ -125,10 +129,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched author, assignee and text' do input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}") - expect_tokens([ - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } - ]) + wait_for_requests + + expect_tokens([author_token(user.name), assignee_token(user.name)]) expect_issues_list_count(3) expect_filtered_search_input(search_term) end @@ -136,10 +139,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched author, assignee, label, and text' do input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'label', value: caps_sensitive_label.title } + author_token(user.name), + assignee_token(user.name), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -148,11 +153,13 @@ describe 'Filter issues', js: true do it 'filters issues by searched author, assignee, label, milestone and text' do input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'milestone', value: milestone.title } + author_token(user.name), + assignee_token(user.name), + label_token(caps_sensitive_label.title), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -169,7 +176,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee' do input_filtered_search("assignee:@#{user.username}") - expect_tokens([{ name: 'assignee', value: user.username }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -177,7 +186,7 @@ describe 'Filter issues', js: true do it 'filters issues by no assignee' do input_filtered_search('assignee:none') - expect_tokens([{ name: 'assignee', value: 'none' }]) + expect_tokens([assignee_token('none')]) expect_issues_list_count(8, 1) expect_filtered_search_input_empty end @@ -197,7 +206,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee and text' do input_filtered_search("assignee:@#{user.username} #{search_term}") - expect_tokens([{ name: 'assignee', value: user.username }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_issues_list_count(2) expect_filtered_search_input(search_term) end @@ -205,10 +216,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee, author and text' do input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}") - expect_tokens([ - { name: 'assignee', value: user.username }, - { name: 'author', value: user.username } - ]) + wait_for_requests + + expect_tokens([assignee_token(user.name), author_token(user.name)]) expect_issues_list_count(2) expect_filtered_search_input(search_term) end @@ -216,10 +226,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee, author, label, text' do input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'assignee', value: user.username }, - { name: 'author', value: user.username }, - { name: 'label', value: caps_sensitive_label.title } + assignee_token(user.name), + author_token(user.name), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -229,10 +241,10 @@ describe 'Filter issues', js: true do input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") expect_tokens([ - { name: 'assignee', value: user.username }, - { name: 'author', value: user.username }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'milestone', value: milestone.title } + assignee_token(user.name), + author_token(user.name), + label_token(caps_sensitive_label.title), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -253,7 +265,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched label' do input_filtered_search("label:~#{bug_label.title}") - expect_tokens([{ name: 'label', value: bug_label.title }]) + expect_tokens([label_token(bug_label.title)]) expect_issues_list_count(2) expect_filtered_search_input_empty end @@ -261,7 +273,7 @@ describe 'Filter issues', js: true do it 'filters issues by no label' do input_filtered_search('label:none') - expect_tokens([{ name: 'label', value: 'none' }]) + expect_tokens([label_token('none', false)]) expect_issues_list_count(9, 1) expect_filtered_search_input_empty end @@ -274,8 +286,8 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title } + label_token(bug_label.title), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input_empty @@ -287,7 +299,8 @@ describe 'Filter issues', js: true do special_issue.labels << special_label input_filtered_search("label:~#{special_label.title}") - expect_tokens([{ name: 'label', value: special_label.title }]) + + expect_tokens([label_token(special_label.title)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -297,7 +310,7 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{new_label.title}") - expect_tokens([{ name: 'label', value: new_label.title }]) + expect_tokens([label_token(new_label.title)]) expect_no_issues_list() expect_filtered_search_input_empty end @@ -311,25 +324,27 @@ describe 'Filter issues', js: true do input_filtered_search("label:~'#{special_multiple_label.title}'") - # filtered search defaults quotations to double quotes - expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }]) + # Check for search results (which makes sure that the page has changed) expect_issues_list_count(1) + # filtered search defaults quotations to double quotes + expect_tokens([label_token("\"#{special_multiple_label.title}\"")]) + expect_filtered_search_input_empty end it 'single quotes' do input_filtered_search("label:~'#{multiple_words_label.title}'") - expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) expect_issues_list_count(1) + expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_filtered_search_input_empty end it 'double quotes' do input_filtered_search("label:~\"#{multiple_words_label.title}\"") - expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) + expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -341,7 +356,7 @@ describe 'Filter issues', js: true do input_filtered_search("label:~'#{double_quotes_label.title}'") - expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }]) + expect_tokens([label_token("'#{double_quotes_label.title}'")]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -353,7 +368,7 @@ describe 'Filter issues', js: true do input_filtered_search("label:~\"#{single_quotes_label.title}\"") - expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }]) + expect_tokens([label_token("\"#{single_quotes_label.title}\"")]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -363,7 +378,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched label and text' do input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}") - expect_tokens([{ name: 'label', value: caps_sensitive_label.title }]) + expect_tokens([label_token(caps_sensitive_label.title)]) expect_issues_list_count(1) expect_filtered_search_input(search_term) end @@ -371,10 +386,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, author and text' do input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") - expect_tokens([ - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username } - ]) + wait_for_requests + + expect_tokens([label_token(caps_sensitive_label.title), author_token(user.name)]) expect_issues_list_count(1) expect_filtered_search_input(search_term) end @@ -382,10 +396,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, author, assignee and text' do input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -395,10 +411,10 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") expect_tokens([ - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'milestone', value: milestone.title } + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -410,8 +426,8 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}") expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title } + label_token(bug_label.title), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -420,10 +436,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, label2, author and text' do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username } + label_token(bug_label.title), + label_token(caps_sensitive_label.title), + author_token(user.name) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -432,11 +450,13 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, label2, author, assignee and text' do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } + label_token(bug_label.title), + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -445,12 +465,14 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, label2, author, assignee, milestone and text' do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'milestone', value: milestone.title } + label_token(bug_label.title), + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -467,7 +489,7 @@ describe 'Filter issues', js: true do end it 'displays in search bar' do - expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) + expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_filtered_search_input_empty end end @@ -484,7 +506,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") - expect_tokens([{ name: 'milestone', value: milestone.title }]) + expect_tokens([milestone_token(milestone.title)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -492,7 +514,7 @@ describe 'Filter issues', js: true do it 'filters issues by no milestone' do input_filtered_search("milestone:none") - expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_tokens([milestone_token('none', false)]) expect_issues_list_count(7, 1) expect_filtered_search_input_empty end @@ -500,7 +522,7 @@ describe 'Filter issues', js: true do it 'filters issues by upcoming milestones' do input_filtered_search("milestone:upcoming") - expect_tokens([{ name: 'milestone', value: 'upcoming' }]) + expect_tokens([milestone_token('upcoming', false)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -508,7 +530,7 @@ describe 'Filter issues', js: true do it 'filters issues by started milestones' do input_filtered_search("milestone:started") - expect_tokens([{ name: 'milestone', value: 'started' }]) + expect_tokens([milestone_token('started', false)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -527,7 +549,7 @@ describe 'Filter issues', js: true do input_filtered_search("milestone:%#{special_milestone.title}") - expect_tokens([{ name: 'milestone', value: special_milestone.title }]) + expect_tokens([milestone_token(special_milestone.title)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -537,7 +559,7 @@ describe 'Filter issues', js: true do input_filtered_search("milestone:%#{new_milestone.title}") - expect_tokens([{ name: 'milestone', value: new_milestone.title }]) + expect_tokens([milestone_token(new_milestone.title)]) expect_no_issues_list() expect_filtered_search_input_empty end @@ -549,7 +571,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone and text' do input_filtered_search("milestone:%#{milestone.title} #{search_term}") - expect_tokens([{ name: 'milestone', value: milestone.title }]) + expect_tokens([milestone_token(milestone.title)]) expect_issues_list_count(2) expect_filtered_search_input(search_term) end @@ -557,9 +579,11 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone, author and text' do input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'milestone', value: milestone.title }, - { name: 'author', value: user.username } + milestone_token(milestone.title), + author_token(user.name) ]) expect_issues_list_count(2) expect_filtered_search_input(search_term) @@ -568,10 +592,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone, author, assignee and text' do input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'milestone', value: milestone.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } + milestone_token(milestone.title), + author_token(user.name), + assignee_token(user.name) ]) expect_issues_list_count(2) expect_filtered_search_input(search_term) @@ -580,11 +606,13 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone, author, assignee, label and text' do input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'milestone', value: milestone.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'label', value: bug_label.title } + milestone_token(milestone.title), + author_token(user.name), + assignee_token(user.name), + label_token(bug_label.title) ]) expect_issues_list_count(2) expect_filtered_search_input(search_term) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index aa9d0d842de..a432d031337 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -32,7 +32,7 @@ describe 'Search bar', js: true do it 'selects item' do filtered_search.native.send_keys(:down, :down, :enter) - expect_tokens([{ name: 'author' }]) + expect_tokens([author_token]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 52efe944b69..14a555fde10 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -346,8 +346,8 @@ describe 'Visual tokens', js: true do it 'tokenizes the search term to complete visual token' do expect_tokens([ - { name: 'author', value: '@root' }, - { name: 'assignee', value: 'none' } + author_token(user.name), + assignee_token('none') ]) end end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 9f08ecc214b..62dbc3efb01 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -133,8 +133,6 @@ feature 'Issue notes polling', :js do def click_edit_action(note) note_element = find("#note_#{note.id}") - open_more_actions_dropdown(note) - note_element.find('.js-note-edit').click end end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 5a7c4f54cb6..bcc6e9bab0f 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -14,7 +14,7 @@ feature 'Multiple issue updating from issues#index', :js do it 'sets to closed' do visit project_issues_path(project) - click_button 'Edit Issues' + click_button 'Edit issues' find('#check-all-issues').click find('.js-issue-status').click @@ -27,7 +27,7 @@ feature 'Multiple issue updating from issues#index', :js do create_closed visit project_issues_path(project, state: 'closed') - click_button 'Edit Issues' + click_button 'Edit issues' find('#check-all-issues').click find('.js-issue-status').click @@ -41,7 +41,7 @@ feature 'Multiple issue updating from issues#index', :js do it 'updates to current user' do visit project_issues_path(project) - click_button 'Edit Issues' + click_button 'Edit issues' find('#check-all-issues').click click_update_assignee_button @@ -57,7 +57,7 @@ feature 'Multiple issue updating from issues#index', :js do create_assigned visit project_issues_path(project) - click_button 'Edit Issues' + click_button 'Edit issues' find('#check-all-issues').click click_update_assignee_button @@ -73,7 +73,7 @@ feature 'Multiple issue updating from issues#index', :js do it 'updates milestone' do visit project_issues_path(project) - click_button 'Edit Issues' + click_button 'Edit issues' find('#check-all-issues').click find('.issues-bulk-update .js-milestone-select').click @@ -89,7 +89,7 @@ feature 'Multiple issue updating from issues#index', :js do expect(first('.issue')).to have_content milestone.title - click_button 'Edit Issues' + click_button 'Edit issues' find('#check-all-issues').click find('.issues-bulk-update .js-milestone-select').click diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a5bb642221c..3ffc80622f5 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -708,7 +708,7 @@ describe 'Issues' do end describe 'confidential issue#show', js: true do - it 'shows confidential sibebar information as confidential and can be turned off' do + it 'shows confidential sibebar information as confidential and can be turned off' do issue = create(:issue, :confidential, project: project) visit project_issue_path(project, issue) @@ -729,7 +729,6 @@ describe 'Issues' do visit project_issue_path(project, issue) expect(page).not_to have_css('.is-confidential') - expect(page).to have_css('.is-not-confidential') end end end diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 2c560632a1b..2d2c674f8fb 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -28,11 +28,12 @@ feature 'Merge request conflict resolution', js: true do end click_button 'Commit conflict resolution' - wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff + wait_for_requests + click_on 'Changes' wait_for_requests @@ -69,10 +70,12 @@ feature 'Merge request conflict resolution', js: true do end click_button 'Commit conflict resolution' - wait_for_requests + expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff + wait_for_requests + click_on 'Changes' wait_for_requests @@ -140,12 +143,13 @@ feature 'Merge request conflict resolution', js: true do end click_button 'Commit conflict resolution' - wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff + wait_for_requests + click_on 'Changes' wait_for_requests click_link 'Expand all' diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 2d9419d6124..c4f02311f13 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -157,7 +157,7 @@ feature 'Diff note avatars', js: true do end page.within find("[id='#{position.line_code(project.repository)}']") do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').trigger('click') expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) expect(find('.diff-comments-more-count')).to have_content '+1' diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index 201be4b9e40..a8f5dc275e4 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -5,7 +5,7 @@ feature 'Diffs URL', js: true do let(:merge_request) { create(:merge_request, source_project: project) } context 'when visit with */* as accept header' do - before(:each) do + before do page.driver.add_header('Accept', '*/*') end diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb index d1cc43e0690..05789bbd31d 100644 --- a/spec/features/merge_requests/discussion_spec.rb +++ b/spec/features/merge_requests/discussion_spec.rb @@ -26,7 +26,7 @@ feature 'Merge Request Discussions' do let(:outdated_diff_refs) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs } - before(:each) do + before do visit project_merge_request_path(project, merge_request) end @@ -71,7 +71,7 @@ feature 'Merge Request Discussions' do end end - before(:each) do + before do visit project_merge_request_path(project, merge_request) end diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index 1d52a4659ad..9912e8165e6 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge Request filtering by Labels', js: true do +feature 'Merge Request filtering by Labels', :js do include FilteredSearchHelpers include MergeRequestHelpers @@ -12,9 +12,9 @@ feature 'Merge Request filtering by Labels', js: true do let!(:feature) { create(:label, project: project, title: 'feature') } let!(:enhancement) { create(:label, project: project, title: 'enhancement') } - let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } - let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } - let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } + let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") } + let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "wip") } + let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "improve/awesome") } before do mr1.labels << bug @@ -25,7 +25,7 @@ feature 'Merge Request filtering by Labels', js: true do mr3.title = "Feature1" mr3.labels << feature - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_merge_requests_path(project) diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index 521fcabc881..166c02a7a7f 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -25,7 +25,7 @@ feature 'Merge Request filtering by Milestone' do visit_merge_requests(project) input_filtered_search('milestone:none') - expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_tokens([milestone_token('none', false)]) expect_filtered_search_input_empty expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index f0019be86ad..b51ae0890e4 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -12,7 +12,7 @@ describe 'Filter merge requests' do let!(:wontfix) { create(:label, project: project, title: "Won't fix") } before do - project.team << [user, :master] + project.add_master(user) group.add_developer(user) sign_in(user) create(:merge_request, source_project: project, target_project: project) @@ -24,7 +24,9 @@ describe 'Filter merge requests' do let(:search_query) { "assignee:@#{user.username}" } def expect_assignee_visual_tokens - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end @@ -57,7 +59,7 @@ describe 'Filter merge requests' do let(:search_query) { "milestone:%\"#{milestone.title}\"" } def expect_milestone_visual_tokens - expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }]) + expect_tokens([milestone_token("\"#{milestone.title}\"")]) expect_filtered_search_input_empty end @@ -91,7 +93,7 @@ describe 'Filter merge requests' do input_filtered_search('label:none') expect_mr_list_count(1) - expect_tokens([{ name: 'label', value: 'none' }]) + expect_tokens([label_token('none', false)]) expect_filtered_search_input_empty end @@ -99,7 +101,7 @@ describe 'Filter merge requests' do input_filtered_search("label:~#{label.title}") expect_mr_list_count(0) - expect_tokens([{ name: 'label', value: "~#{label.title}" }]) + expect_tokens([label_token(label.title)]) expect_filtered_search_input_empty end @@ -107,10 +109,7 @@ describe 'Filter merge requests' do input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") expect_mr_list_count(0) - expect_tokens([ - { name: 'label', value: "~\"#{wontfix.title}\"" }, - { name: 'label', value: "~#{label.title}" } - ]) + expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)]) expect_filtered_search_input_empty end @@ -118,16 +117,13 @@ describe 'Filter merge requests' do input_filtered_search("label:~\"#{wontfix.title}\"") expect_mr_list_count(0) - expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }]) + expect_tokens([label_token("\"#{wontfix.title}\"")]) expect_filtered_search_input_empty input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(0) - expect_tokens([ - { name: 'label', value: "~\"#{wontfix.title}\"" }, - { name: 'label', value: "~#{label.title}" } - ]) + expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)]) expect_filtered_search_input_empty end end @@ -143,10 +139,9 @@ describe 'Filter merge requests' do context 'assignee and label', js: true do def expect_assignee_label_visual_tokens - expect_tokens([ - { name: 'assignee', value: "@#{user.username}" }, - { name: 'label', value: "~#{label.title}" } - ]) + wait_for_requests + + expect_tokens([assignee_token(user.name), label_token(label.title)]) expect_filtered_search_input_empty end @@ -170,7 +165,7 @@ describe 'Filter merge requests' do describe 'filter merge requests by text' do before do - create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") + create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "wip") bug_label = create(:label, project: project, title: 'bug') milestone = create(:milestone, title: "8", project: project) @@ -179,7 +174,7 @@ describe 'Filter merge requests' do title: "Bug 2", source_project: project, target_project: project, - source_branch: "bug2", + source_branch: "fix", milestone: milestone, author: user, assignee: user) @@ -214,7 +209,7 @@ describe 'Filter merge requests' do input_filtered_search_keys(' label:~bug') expect_mr_list_count(1) - expect_tokens([{ name: 'label', value: '~bug' }]) + expect_tokens([label_token('bug')]) expect_filtered_search_input('Bug') end @@ -227,7 +222,7 @@ describe 'Filter merge requests' do input_filtered_search_keys(' milestone:%8') expect_mr_list_count(1) - expect_tokens([{ name: 'milestone', value: '%8' }]) + expect_tokens([milestone_token('8')]) expect_filtered_search_input('Bug') end @@ -240,7 +235,10 @@ describe 'Filter merge requests' do input_filtered_search_keys(" assignee:@#{user.username}") expect_mr_list_count(1) - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input('Bug') end @@ -252,19 +250,21 @@ describe 'Filter merge requests' do input_filtered_search_keys(" author:@#{user.username}") + wait_for_requests + expect_mr_list_count(1) - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input('Bug') end end end - describe 'filter merge requests and sort', js: true do + describe 'filter merge requests and sort', :js do before do bug_label = create(:label, project: project, title: 'bug') - mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") - mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") + mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "wip") + mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "fix") mr1.labels << bug_label mr2.labels << bug_label @@ -293,7 +293,9 @@ describe 'Filter merge requests' do it 'filter by current user' do visit project_merge_requests_path(project, assignee_id: user.id) - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end @@ -303,7 +305,9 @@ describe 'Filter merge requests' do visit project_merge_requests_path(project, assignee_id: new_user.id) - expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }]) + wait_for_requests + + expect_tokens([assignee_token(new_user.name)]) expect_filtered_search_input_empty end end @@ -312,7 +316,9 @@ describe 'Filter merge requests' do it 'filter by current user' do visit project_merge_requests_path(project, author_id: user.id) - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + wait_for_requests + + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end @@ -322,7 +328,9 @@ describe 'Filter merge requests' do visit project_merge_requests_path(project, author_id: new_user.id) - expect_tokens([{ name: 'author', value: "@#{new_user.username}" }]) + wait_for_requests + + expect_tokens([author_token(new_user.name)]) expect_filtered_search_input_empty end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index c1b90e5f875..eed95816bdf 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge requests filter clear button', js: true do +feature 'Merge requests filter clear button', :js do include FilteredSearchHelpers include MergeRequestHelpers include IssueHelpers @@ -9,8 +9,8 @@ feature 'Merge requests filter clear button', js: true do let!(:user) { create(:user) } let!(:milestone) { create(:milestone, project: project) } let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } - let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "improve/awesome", milestone: milestone, author: user, assignee: user) } + let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") } let(:merge_request_css) { '.merge-request' } let(:clear_search_css) { '.filtered-search-box .clear-search' } diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index cf30a687df8..e6dc284cba7 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -98,7 +98,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end def change_status(text) - click_button 'Edit Merge Requests' + click_button 'Edit merge requests' find('#check-all-issues').click find('.js-issue-status').click find('.dropdown-menu-status a', text: text).click @@ -106,7 +106,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end def change_assignee(text) - click_button 'Edit Merge Requests' + click_button 'Edit merge requests' find('#check-all-issues').click find('.js-update-assignee').click wait_for_requests @@ -119,7 +119,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end def change_milestone(text) - click_button 'Edit Merge Requests' + click_button 'Edit merge requests' find('#check-all-issues').click find('.issues-bulk-update .js-milestone-select').click find('.dropdown-menu-milestone a', text: text).click diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index d62b035b40b..20008b4e7f9 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -24,12 +24,10 @@ describe 'Projects > Merge requests > User lists merge requests' do milestone: create(:milestone, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) - # lfs in itself is not a great choice for the title if one wants to match the whole body content later on - # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name create(:merge_request, - title: 'merge_lfs', + title: 'merge-test', source_project: project, - source_branch: 'merge_lfs', + source_branch: 'merge-test', created_at: 3.minutes.ago, updated_at: 10.seconds.ago) end @@ -38,7 +36,7 @@ describe 'Projects > Merge requests > User lists merge requests' do visit_merge_requests(project, assignee_id: IssuableFinder::NONE) expect(current_path).to eq(project_merge_requests_path(project)) - expect(page).to have_content 'merge_lfs' + expect(page).to have_content 'merge-test' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' expect(count_merge_requests).to eq(1) @@ -47,7 +45,7 @@ describe 'Projects > Merge requests > User lists merge requests' do it 'filters on a specific assignee' do visit_merge_requests(project, assignee_id: user.id) - expect(page).not_to have_content 'merge_lfs' + expect(page).not_to have_content 'merge-test' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' expect(count_merge_requests).to eq(2) @@ -57,14 +55,14 @@ describe 'Projects > Merge requests > User lists merge requests' do visit_merge_requests(project, sort: sort_value_recently_created) expect(first_merge_request).to include('fix') - expect(last_merge_request).to include('merge_lfs') + expect(last_merge_request).to include('merge-test') expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do visit_merge_requests(project, sort: sort_value_oldest_created) - expect(first_merge_request).to include('merge_lfs') + expect(first_merge_request).to include('merge-test') expect(last_merge_request).to include('fix') expect(count_merge_requests).to eq(3) end @@ -72,7 +70,7 @@ describe 'Projects > Merge requests > User lists merge requests' do it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) - expect(first_merge_request).to include('merge_lfs') + expect(first_merge_request).to include('merge-test') expect(count_merge_requests).to eq(3) 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 1cfd78663e5..f89dd38e5cd 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -71,7 +71,7 @@ feature 'Merge requests > User posts diff notes', :js do end context 'with an unfolded line' do - before(:each) do + before do find('.js-unfold', match: :first).click wait_for_requests end @@ -120,7 +120,7 @@ feature 'Merge requests > User posts diff notes', :js do end context 'with an unfolded line' do - before(:each) do + before do find('.js-unfold', match: :first).click wait_for_requests end diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index 74d21822a59..d7cda73ab40 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -75,7 +75,6 @@ describe 'Merge requests > User posts notes', :js do describe 'editing the note' do before do find('.note').hover - open_more_actions_dropdown(note) find('.js-note-edit').click end @@ -104,7 +103,6 @@ describe 'Merge requests > User posts notes', :js do wait_for_requests find('.note').hover - open_more_actions_dropdown(note) find('.js-note-edit').click @@ -132,7 +130,6 @@ describe 'Merge requests > User posts notes', :js do describe 'deleting an attachment' do before do find('.note').hover - open_more_actions_dropdown(note) find('.js-note-edit').click end diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb index 20303359c46..624f13922ed 100644 --- a/spec/features/milestones/show_spec.rb +++ b/spec/features/milestones/show_spec.rb @@ -8,7 +8,7 @@ describe 'Milestone show' do let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } } before do - project.add_user(user, :developer) + project.add_user(user, :developer) sign_in(user) end diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index 5e1e7dc078f..b45972b7f6b 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -16,7 +16,7 @@ feature 'Password reset' do user.send_reset_password_instructions user.update_attribute(:reset_password_sent_at, 5.minutes.ago) - expect{ forgot_password(user) }.to change{ user.reset_password_sent_at } + expect { forgot_password(user) }.to change { user.reset_password_sent_at } expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) expect(current_path).to eq new_user_session_path end @@ -27,7 +27,7 @@ feature 'Password reset' do # Reload because PG handles datetime less precisely than Ruby/Rails user.reload - expect{ forgot_password(user) }.not_to change{ user.reset_password_sent_at } + expect { forgot_password(user) }.not_to change { user.reset_password_sent_at } expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) expect(current_path).to eq new_user_session_path end diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index 9944a6e1ff1..dcd0449dbcb 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -35,7 +35,7 @@ feature 'Profile > Account' do TestEnv.clean_test_path end - after(:example) do + after do TestEnv.clean_test_path end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index 9123aa9d155..c935cdfd5c4 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Profile > Preferences' do +describe 'Profile > Preferences', :js do let(:user) { create(:user) } before do @@ -8,28 +8,32 @@ describe 'Profile > Preferences' do visit profile_preferences_path end - describe 'User changes their syntax highlighting theme', js: true do + describe 'User changes their syntax highlighting theme' do it 'creates a flash message' do choose 'user_color_scheme_id_5' + wait_for_requests + expect_preferences_saved_message end it 'updates their preference' do choose 'user_color_scheme_id_5' - allowing_for_delay do - visit page.current_path - expect(page).to have_checked_field('user_color_scheme_id_5') - end + wait_for_requests + refresh + + expect(page).to have_checked_field('user_color_scheme_id_5') end end - describe 'User changes their default dashboard', js: true do + describe 'User changes their default dashboard' do it 'creates a flash message' do select 'Starred Projects', from: 'user_dashboard' click_button 'Save' + wait_for_requests + expect_preferences_saved_message end @@ -37,12 +41,12 @@ describe 'Profile > Preferences' do select 'Starred Projects', from: 'user_dashboard' click_button 'Save' - allowing_for_delay do - find('#logo').click + wait_for_requests + + find('#logo').click - expect(page).to have_content("You don't have starred projects yet") - expect(page.current_path).to eq starred_dashboard_projects_path - end + expect(page).to have_content("You don't have starred projects yet") + expect(page.current_path).to eq starred_dashboard_projects_path find('.shortcuts-activity').trigger('click') diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index 4238d25e9ee..9bcd5beabb8 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -20,7 +20,7 @@ feature 'Template Undo Button', js: true do end end - context 'creating a non-matching file' do + context 'creating a non-matching file' do before do visit project_new_blob_path(project, 'master') select_file_template_type('LICENSE') diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 24e7843db63..6a324d32ca7 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -10,7 +10,7 @@ feature 'Import/Export - project import integration test', js: true do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end - after(:each) do + after do FileUtils.rm_rf(export_path, secure: true) end diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 7d4ec2b4e68..80d91e5915f 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -65,7 +65,7 @@ describe 'Edit Project Settings' do TestEnv.clean_test_path end - after(:example) do + after do TestEnv.clean_test_path end @@ -107,11 +107,11 @@ describe 'Edit Project Settings' do TestEnv.clean_test_path end - before(:example) do + before do group.add_owner(user) end - after(:example) do + after do TestEnv.clean_test_path end diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 8ae89c980b9..3129aad8473 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -1,10 +1,6 @@ require 'spec_helper' describe 'User edits files' do - let(:fork_message) do - "You're not allowed to make changes to this project directly. "\ - "A fork of this project has been created that you can make changes in, so you can submit a merge request." - end let(:project) { create(:project, :repository, name: 'Shop') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } @@ -24,6 +20,9 @@ describe 'User edits files' do it 'inserts a content of a file', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') @@ -39,6 +38,9 @@ describe 'User edits files' do it 'commits an edited file', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') @@ -53,6 +55,9 @@ describe 'User edits files' do it 'commits an edited file to a new branch', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:branch_name, with: 'new_branch_name', visible: true) @@ -69,6 +74,9 @@ describe 'User edits files' do it 'shows the diff of an edited file', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") click_link('Preview changes') @@ -91,7 +99,12 @@ describe 'User edits files' do click_link('Fork') - expect(page).to have_content(fork_message) + expect(page).to have_content( + "You're not allowed to make changes to this project directly. "\ + "A fork of this project has been created that you can make changes in, so you can submit a merge request." + ) + + wait_for_requests execute_script("ace.edit('editor').setValue('*.rbca')") @@ -106,6 +119,9 @@ describe 'User edits files' do expect(page).to have_button('Cancel') click_link('Fork') + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index d3d7915bebf..baf3d29e6c5 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -18,7 +18,7 @@ feature 'Project' do click_button "Create project" end - expect(page).to have_content 'This project Loading..' + expect(page).to have_content template.name end end diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index c9ba1a8c088..8abd4403065 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projected Tags', js: true do +feature 'Protected Tags', js: true do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 9b49fc2225d..6742d77937f 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -195,37 +195,33 @@ describe "Search" do it 'takes user to her issues page when issues assigned is clicked' do find('.dropdown-menu').click_link 'Issues assigned to me' - sleep 2 expect(page).to have_selector('.filtered-search') - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" - sleep 2 expect(page).to have_selector('.filtered-search') - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end it 'takes user to her MR page when MR assigned is clicked' do find('.dropdown-menu').click_link 'Merge requests assigned to me' - sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end it 'takes user to her MR page when MR authored is clicked' do find('.dropdown-menu').click_link "Merge requests I've created" - sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index f1d0905738b..c0c293dee78 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -91,11 +91,7 @@ describe 'Comments on personal snippets', :js do context 'when editing a note' do it 'changes the text' do - open_more_actions_dropdown(snippet_notes[0]) - - page.within("#notes-list li#note_#{snippet_notes[0].id}") do - click_on 'Edit comment' - end + find('.js-note-edit').click page.within('.current-note-edit-form') do fill_in 'note[note]', with: 'new content' diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index a919f5fa20b..d732383a1e1 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -41,7 +41,7 @@ feature 'User creates snippet', :js do expect(page).to have_content('My Snippet') link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/temp/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z}) visit(link) expect(page.status_code).to eq(200) @@ -59,7 +59,7 @@ feature 'User creates snippet', :js do wait_for_requests link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) visit(link) expect(page.status_code).to eq(200) @@ -84,7 +84,7 @@ feature 'User creates snippet', :js do end expect(page).to have_content('Hello World!') link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) visit(link) expect(page.status_code).to eq(200) diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb index 26070e508e2..71de6b6bd1c 100644 --- a/spec/features/snippets/user_edits_snippet_spec.rb +++ b/spec/features/snippets/user_edits_snippet_spec.rb @@ -33,7 +33,7 @@ feature 'User edits snippet', :js do wait_for_requests link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) end it 'updates the snippet to make it internal' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index c14826df55a..580258f77eb 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -52,8 +52,8 @@ feature 'Task Lists' do before do Warden.test_mode! - project.team << [user, :master] - project.team << [user2, :guest] + project.add_master(user) + project.add_guest(user2) login_as(user) end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 8d12a492feb..47664de469a 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -82,7 +82,7 @@ feature 'Triggers', js: true do end describe 'trigger "Take ownership" workflow' do - before(:each) do + before do create(:ci_trigger, owner: user2, project: @project, description: trigger_title) visit project_settings_ci_cd_path(@project) end @@ -104,7 +104,7 @@ feature 'Triggers', js: true do end describe 'trigger "Revoke" workflow' do - before(:each) do + before do create(:ci_trigger, owner: user2, project: @project, description: trigger_title) visit project_settings_ci_cd_path(@project) end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index ff004d85272..15b89dac572 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -73,7 +73,7 @@ feature 'Users', js: true do let(:loading_icon) { '.fa.fa-spinner' } let(:username_input) { 'new_user_username' } - before(:each) do + before do visit new_user_session_path click_link 'Register' end @@ -104,7 +104,7 @@ feature 'Users', js: true do end def errors_on_page(page) - page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n") + page.find('#error_explanation').find('ul').all('li').map { |item| item.text }.join("\n") end def number_of_errors_on_page(page) diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb index 4e367d39cf3..28e36330029 100644 --- a/spec/finders/admin/projects_finder_spec.rb +++ b/spec/finders/admin/projects_finder_spec.rb @@ -38,6 +38,12 @@ describe Admin::ProjectsFinder do it { is_expected.to match_array([shared_project, public_project, internal_project, private_project]) } end + context 'with pending delete project' do + let!(:pending_delete_project) { create(:project, pending_delete: true) } + + it { is_expected.not_to include(pending_delete_project) } + end + context 'filter by namespace_id' do let(:namespace) { create(:namespace) } let!(:project_in_namespace) { create(:project, namespace: namespace) } diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index 2d079ea83b4..60ea98e61c7 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -14,8 +14,8 @@ describe ContributedProjectsFinder do private_project.add_developer(current_user) public_project.add_master(source_user) - create(:event, :pushed, project: public_project, target: public_project, author: source_user) - create(:event, :pushed, project: private_project, target: private_project, author: source_user) + create(:push_event, project: public_project, author: source_user) + create(:push_event, project: private_project, author: source_user) end describe 'without a current user' do diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb index 0c063f6d5ee..3a8a1e7de74 100644 --- a/spec/finders/environments_finder_spec.rb +++ b/spec/finders/environments_finder_spec.rb @@ -12,7 +12,7 @@ describe EnvironmentsFinder do context 'tagged deployment' do before do - create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) + create(:deployment, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) end it 'returns environment when with_tags is set' do diff --git a/spec/fixtures/encoding/Japanese.md b/spec/fixtures/encoding/Japanese.md new file mode 100644 index 00000000000..dd469c9f232 --- /dev/null +++ b/spec/fixtures/encoding/Japanese.md @@ -0,0 +1,42 @@ ++++ +date = "2017-05-21T13:05:07+09:00" +title = "レイヤ" +weight = 10 + ++++ + +## このチュートリアルで扱う内容 +1. Redactedにおける2D開発でのレイヤの基本的な概要 +2. スクリーン上のスプライトの順序付け方法 + +### Redactedにおける2D開発でのレイヤの基本的な概要 +2Dにおいてはz軸が存在しないため、シーン内要素の描画順を制御するためには代替となる仕組みが必要です。 +Redactedでは**レイヤ**における**zIndex**属性を制御可能にすることで、この課題を解決しています。 +**デフォルトでは、zIndexは0となりオブジェクトはレイヤに追加された順番に描画されます。** + +レイヤにはいくつかの重要な特性があります。 + +* レイヤにはレイヤ化されたオブジェクトのみを含めることができます。(**3Dモデルは絶対に追加しないでください**) +* レイヤはレイヤ化されたオブジェクトです。(したがって、レイヤには他のレイヤを含めることができます) +* レイヤ化されたオブジェクトは、最大で1つのレイヤに属すことができます。 + +レイヤを直接初期化することはできませんが、その派生クラスは初期化することが可能です。**Scene2D**と**コンテナ**は、**レイヤ**から派生する2つの主なオブジェクトです。すべての初期化(createContainer、instantiate、...)はレイヤ上で行われます。つまり、2Dで初期化されるすべてのオブジェクトは、zIndexプロパティを持つレイヤ化されたオブジェクトです。 + +**zIndexはグローバルではありません!** + +CSSとは異なり、zIndexはすべてのオブジェクトに対してグローバルではありません。zIndexプロパティは親レイヤに対してローカルです。詳細につきましては、コンテナチュートリアルで説明しています。 [TODO: Link]。 + +### スクリーン上のスプライトの順序付け方法 +これまで学んだことを生かして、画面にスプライトを表示して、zIndexの設定をしてみましょう! + +* まず、最初に (A,B,C) スプライトを生成します。 +* スプライトAをシーンに追加します(zIndex = 0、標準色) +* スプライトBをシーン2に追加すると、**スプライトAの上に**表示されます(zIndex = 0、赤色) +* 最後にスプライトCをシーンに追加します(青色)が、スプライトのzIndexを-1に設定すると、スプライトはAとBの後側に表示されます。 + +{{< code "static/tutorials/layers.html" >}} + +### ソースコード全体 +```js +{{< snippet "static/tutorials/layers.html" >}} +``` diff --git a/spec/helpers/pagination_helper_spec.rb b/spec/helpers/pagination_helper_spec.rb new file mode 100644 index 00000000000..e235475fb47 --- /dev/null +++ b/spec/helpers/pagination_helper_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe PaginationHelper do + describe '#paginate_collection' do + let(:collection) { User.all.page(1) } + + it 'paginates a collection without using a COUNT' do + without_count = collection.without_count + + expect(helper).to receive(:paginate_without_count) + .with(without_count) + .and_call_original + + helper.paginate_collection(without_count) + end + + it 'paginates a collection using a COUNT' do + expect(helper).to receive(:paginate_with_count).and_call_original + + helper.paginate_collection(collection) + end + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 37a5e6b474e..d1efa318d14 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -432,9 +432,7 @@ describe ProjectsHelper do end describe '#any_projects?' do - before do - create(:project) - end + let!(:project) { create(:project) } it 'returns true when projects will be returned' do expect(helper.any_projects?(Project.all)).to eq(true) @@ -444,6 +442,14 @@ describe ProjectsHelper do expect(helper.any_projects?(Project.none)).to eq(false) end + it 'returns true when using a non-empty Array' do + expect(helper.any_projects?([project])).to eq(true) + end + + it 'returns false when using an empty Array' do + expect(helper.any_projects?([])).to eq(false) + end + it 'only executes a single query when a LIMIT is applied' do relation = Project.limit(1) recorder = ActiveRecord::QueryRecorder.new do diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb index 889fe441171..5eba03ef576 100644 --- a/spec/helpers/version_check_helper_spec.rb +++ b/spec/helpers/version_check_helper_spec.rb @@ -23,7 +23,7 @@ describe VersionCheckHelper do end it 'should have a js prefixed css class' do - expect(@image_tag).to match(/class="js-version-status-badge"/) + expect(@image_tag).to match(/class="js-version-status-badge lazy"/) end it 'should have a VersionCheck url as the src' do diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js new file mode 100644 index 00000000000..2c8183ff77b --- /dev/null +++ b/spec/javascripts/blob/blob_file_dropzone_spec.js @@ -0,0 +1,42 @@ +import 'dropzone'; +import BlobFileDropzone from '~/blob/blob_file_dropzone'; + +describe('BlobFileDropzone', () => { + preloadFixtures('blob/show.html.raw'); + + beforeEach(() => { + loadFixtures('blob/show.html.raw'); + const form = $('.js-upload-blob-form'); + this.blobFileDropzone = new BlobFileDropzone(form, 'POST'); + this.dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone; + this.replaceFileButton = $('#submit-all'); + }); + + describe('submit button', () => { + it('requires file', () => { + spyOn(window, 'alert'); + + this.replaceFileButton.click(); + + expect(window.alert).toHaveBeenCalled(); + }); + + it('is disabled while uploading', () => { + spyOn(window, 'alert'); + + const file = { + name: 'some-file.jpg', + type: 'jpg', + }; + const fakeEvent = jQuery.Event('drop', { + dataTransfer: { files: [file] }, + }); + + this.dropzone.listeners[0].events.drop(fakeEvent); + this.replaceFileButton.click(); + + expect(window.alert).not.toHaveBeenCalled(); + expect(this.replaceFileButton.is(':disabled')).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/breakpoints_spec.js b/spec/javascripts/breakpoints_spec.js new file mode 100644 index 00000000000..b1b5d36c1fb --- /dev/null +++ b/spec/javascripts/breakpoints_spec.js @@ -0,0 +1,15 @@ +import bp, { + breakpoints, +} from '~/breakpoints'; + +describe('breakpoints', () => { + Object.keys(breakpoints).forEach((key) => { + const size = breakpoints[key]; + + it(`returns ${key} when larger than ${size}`, () => { + spyOn(bp, 'windowWidth').and.returnValue(size + 10); + + expect(bp.getBreakpointSize()).toBe(key); + }); + }); +}); diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb index de673f94d72..387858cba77 100644 --- a/spec/javascripts/fixtures/abuse_reports.rb +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -14,7 +14,7 @@ describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controll clean_frontend_fixtures('abuse_reports/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb new file mode 100644 index 00000000000..2dffc42b0ef --- /dev/null +++ b/spec/javascripts/fixtures/blob.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('blob/') + end + + before do + sign_in(admin) + end + + it 'blob/show.html.raw' do |example| + get(:show, + namespace_id: project.namespace, + project_id: project, + id: 'add-ipython-files/files/ipython/basic.ipynb') + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb index d7c3dc0a235..494c9cabdcc 100644 --- a/spec/javascripts/fixtures/boards.rb +++ b/spec/javascripts/fixtures/boards.rb @@ -13,7 +13,7 @@ describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller clean_frontend_fixtures('boards/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb index a059818145b..bb3bdf7c215 100644 --- a/spec/javascripts/fixtures/branches.rb +++ b/spec/javascripts/fixtures/branches.rb @@ -13,7 +13,7 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle clean_frontend_fixtures('branches/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb index e83db8daaf2..793ffa7c220 100644 --- a/spec/javascripts/fixtures/dashboard.rb +++ b/spec/javascripts/fixtures/dashboard.rb @@ -13,7 +13,7 @@ describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controll clean_frontend_fixtures('dashboard/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb index fca3f5b1bfe..bea161c514f 100644 --- a/spec/javascripts/fixtures/deploy_keys.rb +++ b/spec/javascripts/fixtures/deploy_keys.rb @@ -12,7 +12,7 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control clean_frontend_fixtures('deploy_keys/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/environments.rb b/spec/javascripts/fixtures/environments.rb index 3474f4696ef..d2457d75419 100644 --- a/spec/javascripts/fixtures/environments.rb +++ b/spec/javascripts/fixtures/environments.rb @@ -14,7 +14,7 @@ describe Projects::EnvironmentsController, '(JavaScript fixtures)', type: :contr clean_frontend_fixtures('environments/metrics') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 1a30909977e..d3ad50af1b9 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -13,7 +13,7 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller clean_frontend_fixtures('issues/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb index dc7dde1138c..83a96797506 100644 --- a/spec/javascripts/fixtures/jobs.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -17,7 +17,7 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('builds/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/labels.rb b/spec/javascripts/fixtures/labels.rb index 2e4811b64a4..814f065f3a4 100644 --- a/spec/javascripts/fixtures/labels.rb +++ b/spec/javascripts/fixtures/labels.rb @@ -22,7 +22,7 @@ describe 'Labels (JavaScript fixtures)' do describe Groups::LabelsController, '(JavaScript fixtures)', type: :controller do render_views - before(:each) do + before do sign_in(admin) end @@ -39,7 +39,7 @@ describe 'Labels (JavaScript fixtures)' do describe Projects::LabelsController, '(JavaScript fixtures)', type: :controller do render_views - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index f9d8b5c569c..f97a5d2b5de 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -33,7 +33,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont clean_frontend_fixtures('merge_requests/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb index 4481a187f63..6e0a97d2e3f 100644 --- a/spec/javascripts/fixtures/merge_requests_diffs.rb +++ b/spec/javascripts/fixtures/merge_requests_diffs.rb @@ -25,7 +25,7 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type clean_frontend_fixtures('merge_request_diffs/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb index daafbac86db..bb85da50f0f 100644 --- a/spec/javascripts/fixtures/pipelines.rb +++ b/spec/javascripts/fixtures/pipelines.rb @@ -19,7 +19,7 @@ describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controll clean_frontend_fixtures('pipelines/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 6c33b240e5c..f09d44a49d1 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -13,7 +13,7 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('projects/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb index 3200577b326..7968c9425f2 100644 --- a/spec/javascripts/fixtures/prometheus_service.rb +++ b/spec/javascripts/fixtures/prometheus_service.rb @@ -7,14 +7,14 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } let!(:service) { create(:prometheus_service, project: project) } - + render_views before(:all) do clean_frontend_fixtures('services/prometheus') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb index 554451d1bbf..80915c32a74 100644 --- a/spec/javascripts/fixtures/services.rb +++ b/spec/javascripts/fixtures/services.rb @@ -7,7 +7,6 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } - render_views @@ -15,7 +14,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle clean_frontend_fixtures('services/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index cc825c82190..01bfb87b0c1 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -14,7 +14,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('snippets/') end - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb index a81ef8c5492..ba630365c18 100644 --- a/spec/javascripts/fixtures/todos.rb +++ b/spec/javascripts/fixtures/todos.rb @@ -18,7 +18,7 @@ describe 'Todos (JavaScript fixtures)' do describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do render_views - before(:each) do + before do sign_in(admin) end @@ -33,7 +33,7 @@ describe 'Todos (JavaScript fixtures)' do describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do render_views - before(:each) do + before do sign_in(admin) end diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index ab74f3e00ec..2e81a1b056b 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -1,10 +1,18 @@ -/* global bp */ +import Cookies from 'js-cookie'; import { calculateTop, - hideSubLevelItems, showSubLevelItems, canShowSubItems, + canShowActiveSubItems, + mouseEnterTopItems, + mouseLeaveTopItem, + setOpenMenu, + mousePos, + getHideSubItemsInterval, + documentMouseMove, + getHeaderHeight, } from '~/fly_out_nav'; +import bp from '~/breakpoints'; describe('Fly out sidebar navigation', () => { let el; @@ -16,11 +24,14 @@ describe('Fly out sidebar navigation', () => { document.body.appendChild(el); spyOn(bp, 'getBreakpointSize').and.callFake(() => breakpointSize); + + setOpenMenu(null); }); afterEach(() => { - el.remove(); + document.body.innerHTML = ''; breakpointSize = 'lg'; + mousePos.length = 0; }); describe('calculateTop', () => { @@ -47,61 +58,153 @@ describe('Fly out sidebar navigation', () => { }); }); - describe('hideSubLevelItems', () => { + describe('getHideSubItemsInterval', () => { beforeEach(() => { - el.innerHTML = '<div class="sidebar-sub-level-items"></div>'; + el.innerHTML = '<div class="sidebar-sub-level-items" style="position: fixed; top: 0; left: 100px; height: 150px;"></div>'; + }); + + it('returns 0 if currentOpenMenu is nil', () => { + expect( + getHideSubItemsInterval(), + ).toBe(0); }); - it('hides subitems', () => { - hideSubLevelItems(el); + it('returns 0 when mouse above sub-items', () => { + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top - 50, + }); expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('none'); + getHideSubItemsInterval(), + ).toBe(0); }); - it('does not hude subitems on mobile', () => { - breakpointSize = 'sm'; + it('returns 0 when mouse is below sub-items', () => { + const subItems = el.querySelector('.sidebar-sub-level-items'); - hideSubLevelItems(el); + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: (el.getBoundingClientRect().top - subItems.getBoundingClientRect().height) + 50, + }); expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).not.toBe('none'); + getHideSubItemsInterval(), + ).toBe(0); }); - it('removes is-over class', () => { + it('returns 300 when mouse is moved towards sub-items', () => { + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left + 20, + clientY: el.getBoundingClientRect().top + 10, + }); + console.log(el); + + expect( + getHideSubItemsInterval(), + ).toBe(300); + }); + }); + + describe('mouseLeaveTopItem', () => { + beforeEach(() => { spyOn(el.classList, 'remove'); + }); - hideSubLevelItems(el); + it('removes is-over class if currentOpenMenu is null', () => { + mouseLeaveTopItem(el); expect( el.classList.remove, ).toHaveBeenCalledWith('is-over'); }); - it('removes is-above class from sub-items', () => { - const subItems = el.querySelector('.sidebar-sub-level-items'); + it('removes is-over class if currentOpenMenu is null & there are sub-items', () => { + el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute;"></div>'; + + mouseLeaveTopItem(el); - spyOn(subItems.classList, 'remove'); + expect( + el.classList.remove, + ).toHaveBeenCalledWith('is-over'); + }); + + it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => { + el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute;"></div>'; - hideSubLevelItems(el); + setOpenMenu(el.querySelector('.sidebar-sub-level-items')); + mouseLeaveTopItem(el); expect( - subItems.classList.remove, - ).toHaveBeenCalledWith('is-above'); + el.classList.remove, + ).not.toHaveBeenCalled(); }); + }); - it('does nothing if el has no sub-items', () => { - el.innerHTML = ''; + describe('mouseEnterTopItems', () => { + beforeEach(() => { + jasmine.clock().install(); - spyOn(el.classList, 'remove'); + el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>'; + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); - hideSubLevelItems(el); + it('shows sub-items after 0ms if no menu is open', () => { + mouseEnterTopItems(el); expect( - el.classList.remove, - ).not.toHaveBeenCalledWith(); + getHideSubItemsInterval(), + ).toBe(0); + + jasmine.clock().tick(0); + + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); + }); + + it('shows sub-items after 300ms if a menu is currently open', () => { + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + + setOpenMenu(el.querySelector('.sidebar-sub-level-items')); + + documentMouseMove({ + clientX: el.getBoundingClientRect().left + 20, + clientY: el.getBoundingClientRect().top + 10, + }); + + mouseEnterTopItems(el); + + expect( + getHideSubItemsInterval(), + ).toBe(300); + + jasmine.clock().tick(300); + + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); }); }); @@ -121,7 +224,7 @@ describe('Fly out sidebar navigation', () => { }); it('does not show sub-items on mobile', () => { - breakpointSize = 'sm'; + breakpointSize = 'xs'; showSubLevelItems(el); @@ -130,7 +233,7 @@ describe('Fly out sidebar navigation', () => { ).not.toBe('block'); }); - it('does not shows sub-items', () => { + it('shows sub-items', () => { showSubLevelItems(el); expect( @@ -144,7 +247,7 @@ describe('Fly out sidebar navigation', () => { expect( subItems.style.transform, - ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top)}px, 0px)`); + ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); }); it('sets is-above when element is above', () => { @@ -170,11 +273,59 @@ describe('Fly out sidebar navigation', () => { }); it('returns false if on mobile size', () => { - breakpointSize = 'sm'; + breakpointSize = 'xs'; expect( canShowSubItems(), ).toBeFalsy(); }); }); + + describe('canShowActiveSubItems', () => { + afterEach(() => { + Cookies.remove('sidebar_collapsed'); + }); + + it('returns true by default', () => { + expect( + canShowActiveSubItems(el), + ).toBeTruthy(); + }); + + it('returns false when cookie is false & element is active', () => { + Cookies.set('sidebar_collapsed', 'false'); + el.classList.add('active'); + + expect( + canShowActiveSubItems(el), + ).toBeFalsy(); + }); + + it('returns true when cookie is false & element is active', () => { + Cookies.set('sidebar_collapsed', 'true'); + el.classList.add('active'); + + expect( + canShowActiveSubItems(el), + ).toBeTruthy(); + }); + + it('returns true when element is active & breakpoint is sm', () => { + breakpointSize = 'sm'; + el.classList.add('active'); + + expect( + canShowActiveSubItems(el), + ).toBeTruthy(); + }); + + it('returns true when element is active & breakpoint is md', () => { + breakpointSize = 'md'; + el.classList.add('active'); + + expect( + canShowActiveSubItems(el), + ).toBeTruthy(); + }); + }); }); diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js new file mode 100644 index 00000000000..7a826487bf9 --- /dev/null +++ b/spec/javascripts/gpg_badges_spec.js @@ -0,0 +1,48 @@ +import GpgBadges from '~/gpg_badges'; + +describe('GpgBadges', () => { + const dummyCommitSha = 'n0m0rec0ffee'; + const dummyBadgeHtml = 'dummy html'; + const dummyResponse = { + signatures: [{ + commit_sha: dummyCommitSha, + html: dummyBadgeHtml, + }], + }; + + beforeEach(() => { + setFixtures(` + <div class="parent-container"> + <div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div> + </div> + `); + }); + + it('displays a loading spinner', () => { + spyOn($, 'get').and.returnValue({ + done() { + // intentionally left blank + }, + }); + + GpgBadges.fetch(); + + expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); + const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); + expect(spinners.length).toBe(1); + }); + + it('replaces the loading spinner', () => { + spyOn($, 'get').and.returnValue({ + done(callback) { + callback(dummyResponse); + }, + }); + + GpgBadges.fetch(); + + expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); + const parentContainer = document.querySelector('.parent-container'); + expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + }); +}); diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js index 6120d224ac0..ed481cb60a1 100644 --- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js +++ b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; -import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout'; +import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue'; const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); const cookieKey = 'pipeline_schedules_callout_dismissed'; diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index db2b7d51626..249a2f36fcd 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -1,57 +1,57 @@ import Vue from 'vue'; import repoCommitSection from '~/repo/components/repo_commit_section.vue'; import RepoStore from '~/repo/stores/repo_store'; -import RepoHelper from '~/repo/helpers/repo_helper'; import Api from '~/api'; describe('RepoCommitSection', () => { const branch = 'master'; const projectUrl = 'projectUrl'; - const openedFiles = [{ + const changedFiles = [{ id: 0, changed: true, url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, + path: 'dir/file0.ext', newContent: 'a', }, { id: 1, changed: true, url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`, + path: 'dir/file1.ext', newContent: 'b', - }, { + }]; + const openedFiles = changedFiles.concat([{ id: 2, url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, + path: 'dir/file2.ext', changed: false, - }]; + }]); RepoStore.projectUrl = projectUrl; - function createComponent() { + function createComponent(el) { const RepoCommitSection = Vue.extend(repoCommitSection); - return new RepoCommitSection().$mount(); + return new RepoCommitSection().$mount(el); } it('renders a commit section', () => { RepoStore.isCommitable = true; + RepoStore.currentBranch = branch; RepoStore.targetBranch = branch; RepoStore.openedFiles = openedFiles; - spyOn(RepoHelper, 'getBranch').and.returnValue(branch); - const vm = createComponent(); - const changedFiles = [...vm.$el.querySelectorAll('.changed-files > li')]; + const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')]; const commitMessage = vm.$el.querySelector('#commit-message'); - const submitCommit = vm.$el.querySelector('.submit-commit'); + const submitCommit = vm.$refs.submitCommit; const targetBranch = vm.$el.querySelector('.target-branch'); expect(vm.$el.querySelector(':scope > form')).toBeTruthy(); - expect(vm.$el.querySelector('.staged-files').textContent).toEqual('Staged files (2)'); - expect(changedFiles.length).toEqual(2); + expect(vm.$el.querySelector('.staged-files').textContent.trim()).toEqual('Staged files (2)'); + expect(changedFileElements.length).toEqual(2); - changedFiles.forEach((changedFile, i) => { - const filePath = RepoHelper.getFilePathFromFullPath(openedFiles[i].url, branch); - - expect(changedFile.textContent).toEqual(filePath); + changedFileElements.forEach((changedFile, i) => { + expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path); }); expect(commitMessage.tagName).toEqual('TEXTAREA'); @@ -59,9 +59,9 @@ describe('RepoCommitSection', () => { expect(submitCommit.type).toEqual('submit'); expect(submitCommit.disabled).toBeTruthy(); expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy(); - expect(vm.$el.querySelector('.commit-summary').textContent).toEqual('Commit 2 files'); - expect(targetBranch.querySelector(':scope > label').textContent).toEqual('Target branch'); - expect(targetBranch.querySelector('.help-block').textContent).toEqual(branch); + expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files'); + expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch'); + expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch); }); it('does not render if not isCommitable', () => { @@ -89,14 +89,20 @@ describe('RepoCommitSection', () => { const projectId = 'projectId'; const commitMessage = 'commitMessage'; RepoStore.isCommitable = true; + RepoStore.currentBranch = branch; + RepoStore.targetBranch = branch; RepoStore.openedFiles = openedFiles; RepoStore.projectId = projectId; - spyOn(RepoHelper, 'getBranch').and.returnValue(branch); + // We need to append to body to get form `submit` events working + // Otherwise we run into, "Form submission canceled because the form is not connected" + // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm + const el = document.createElement('div'); + document.body.appendChild(el); - const vm = createComponent(); + const vm = createComponent(el); const commitMessageEl = vm.$el.querySelector('#commit-message'); - const submitCommit = vm.$el.querySelector('.submit-commit'); + const submitCommit = vm.$refs.submitCommit; vm.commitMessage = commitMessage; @@ -124,10 +130,8 @@ describe('RepoCommitSection', () => { expect(actions[1].action).toEqual('update'); expect(actions[0].content).toEqual(openedFiles[0].newContent); expect(actions[1].content).toEqual(openedFiles[1].newContent); - expect(actions[0].file_path) - .toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[0].url, branch)); - expect(actions[1].file_path) - .toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[1].url, branch)); + expect(actions[0].file_path).toEqual(openedFiles[0].path); + expect(actions[1].file_path).toEqual(openedFiles[1].path); done(); }); @@ -140,7 +144,6 @@ describe('RepoCommitSection', () => { const vm = { submitCommitsLoading: true, changedFiles: new Array(10), - openedFiles: new Array(10), commitMessage: 'commitMessage', editMode: true, }; @@ -149,7 +152,6 @@ describe('RepoCommitSection', () => { expect(vm.submitCommitsLoading).toEqual(false); expect(vm.changedFiles).toEqual([]); - expect(vm.openedFiles).toEqual([]); expect(vm.commitMessage).toEqual(''); expect(vm.editMode).toEqual(false); }); diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js index df2f9697acc..29dc2d21e4b 100644 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ b/spec/javascripts/repo/components/repo_edit_button_spec.js @@ -12,18 +12,22 @@ describe('RepoEditButton', () => { it('renders an edit button that toggles the view state', (done) => { RepoStore.isCommitable = true; RepoStore.changedFiles = []; + RepoStore.binary = false; + RepoStore.openedFiles = [{}, {}]; const vm = createComponent(); expect(vm.$el.tagName).toEqual('BUTTON'); expect(vm.$el.textContent).toMatch('Edit'); - spyOn(vm, 'editClicked').and.callThrough(); + spyOn(vm, 'editCancelClicked').and.callThrough(); + spyOn(vm, 'toggleProjectRefsForm'); vm.$el.click(); Vue.nextTick(() => { - expect(vm.editClicked).toHaveBeenCalled(); + expect(vm.editCancelClicked).toHaveBeenCalled(); + expect(vm.toggleProjectRefsForm).toHaveBeenCalled(); expect(vm.$el.textContent).toMatch('Cancel edit'); done(); }); @@ -38,14 +42,10 @@ describe('RepoEditButton', () => { }); describe('methods', () => { - describe('editClicked', () => { - it('sets dialog to open when there are changedFiles', () => { + describe('editCancelClicked', () => { + it('sets dialog to open when there are changedFiles'); - }); - - it('toggles editMode and calls toggleBlobView', () => { - - }); + it('toggles editMode and calls toggleBlobView'); }); }); }); diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index 35e0c995163..85d55d171f9 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -1,26 +1,49 @@ import Vue from 'vue'; import repoEditor from '~/repo/components/repo_editor.vue'; -import RepoStore from '~/repo/stores/repo_store'; describe('RepoEditor', () => { - function createComponent() { + beforeEach(() => { const RepoEditor = Vue.extend(repoEditor); - return new RepoEditor().$mount(); - } + this.vm = new RepoEditor().$mount(); + }); + + it('renders an ide container', (done) => { + this.vm.openedFiles = ['idiidid']; + this.vm.binary = false; - it('renders an ide container', () => { - const monacoInstance = jasmine.createSpyObj('monacoInstance', ['onMouseUp', 'onKeyUp', 'setModel', 'updateOptions']); - const monaco = { - editor: jasmine.createSpyObj('editor', ['create']), - }; - RepoStore.monaco = monaco; + Vue.nextTick(() => { + expect(this.vm.shouldHideEditor).toBe(false); + expect(this.vm.$el.id).toEqual('ide'); + expect(this.vm.$el.tagName).toBe('DIV'); + done(); + }); + }); - monaco.editor.create.and.returnValue(monacoInstance); - spyOn(repoEditor.watch, 'blobRaw'); + describe('when there are no open files', () => { + it('does not render the ide', (done) => { + this.vm.openedFiles = []; + + Vue.nextTick(() => { + expect(this.vm.shouldHideEditor).toBe(true); + expect(this.vm.$el.tagName).not.toBeDefined(); + done(); + }); + }); + }); - const vm = createComponent(); + describe('when open file is binary and not raw', () => { + it('does not render the IDE', (done) => { + this.vm.binary = true; + this.vm.activeFile = { + raw: false, + }; - expect(vm.$el.id).toEqual('ide'); + Vue.nextTick(() => { + expect(this.vm.shouldHideEditor).toBe(true); + expect(this.vm.$el.tagName).not.toBeDefined(); + done(); + }); + }); }); }); diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js index e1f25e4485f..dfab51710c3 100644 --- a/spec/javascripts/repo/components/repo_file_buttons_spec.js +++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js @@ -23,6 +23,7 @@ describe('RepoFileButtons', () => { RepoStore.activeFile = activeFile; RepoStore.activeFileLabel = activeFileLabel; RepoStore.editMode = true; + RepoStore.binary = false; const vm = createComponent(); const raw = vm.$el.querySelector('.raw'); @@ -31,13 +32,13 @@ describe('RepoFileButtons', () => { expect(vm.$el.id).toEqual('repo-file-buttons'); expect(raw.href).toMatch(`/${activeFile.raw_path}`); - expect(raw.textContent).toEqual('Raw'); + expect(raw.textContent.trim()).toEqual('Raw'); expect(blame.href).toMatch(`/${activeFile.blame_path}`); - expect(blame.textContent).toEqual('Blame'); + expect(blame.textContent.trim()).toEqual('Blame'); expect(history.href).toMatch(`/${activeFile.commits_path}`); - expect(history.textContent).toEqual('History'); - expect(vm.$el.querySelector('.permalink').textContent).toEqual('Permalink'); - expect(vm.$el.querySelector('.preview').textContent).toEqual(activeFileLabel); + expect(history.textContent.trim()).toEqual('History'); + expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); + expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel); }); it('triggers rawPreviewToggle on preview click', () => { @@ -71,12 +72,4 @@ describe('RepoFileButtons', () => { expect(vm.$el.querySelector('.preview')).toBeFalsy(); }); - - it('does not render if not isMini', () => { - RepoStore.openedFiles = []; - - const vm = createComponent(); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); }); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 90616ae13ca..518a2d25ecf 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -39,9 +39,9 @@ describe('RepoFile', () => { expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px'); expect(name.title).toEqual(file.url); expect(name.href).toMatch(`/${file.url}`); - expect(name.textContent).toEqual(file.name); - expect(vm.$el.querySelector('.commit-message').textContent).toBe(file.lastCommitMessage); - expect(vm.$el.querySelector('.commit-update').textContent).toBe(updated); + expect(name.textContent.trim()).toEqual(file.name); + expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage); + expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated); expect(fileIcon.classList.contains(file.icon)).toBeTruthy(); expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`); }); diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js index d84f4c5609e..a030314d749 100644 --- a/spec/javascripts/repo/components/repo_loading_file_spec.js +++ b/spec/javascripts/repo/components/repo_loading_file_spec.js @@ -13,7 +13,7 @@ describe('RepoLoadingFile', () => { function assertLines(lines) { lines.forEach((line, n) => { const index = n + 1; - expect(line.classList.contains(`line-of-code-${index}`)).toBeTruthy(); + expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy(); }); } diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 0d216c9c026..abcff8e537e 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import Helper from '~/repo/helpers/repo_helper'; +import RepoService from '~/repo/services/repo_service'; import RepoStore from '~/repo/stores/repo_store'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; @@ -13,6 +15,7 @@ describe('RepoSidebar', () => { RepoStore.files = [{ id: 0, }]; + RepoStore.openedFiles = []; const vm = createComponent(); const thead = vm.$el.querySelector('thead'); const tbody = vm.$el.querySelector('tbody'); @@ -58,4 +61,51 @@ describe('RepoSidebar', () => { expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); }); + + describe('methods', () => { + describe('fileClicked', () => { + it('should fetch data for new file', () => { + spyOn(Helper, 'getContent').and.callThrough(); + const file1 = { + id: 0, + url: '', + }; + RepoStore.files = [file1]; + RepoStore.isRoot = true; + const vm = createComponent(); + + vm.fileClicked(file1); + + expect(Helper.getContent).toHaveBeenCalledWith(file1); + }); + + it('should hide files in directory if already open', () => { + spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); + const file1 = { + id: 0, + type: 'tree', + url: '', + opened: true, + }; + RepoStore.files = [file1]; + RepoStore.isRoot = true; + const vm = createComponent(); + + vm.fileClicked(file1); + + expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1); + }); + }); + + describe('goToPreviousDirectoryClicked', () => { + it('should hide files in directory if already open', () => { + const prevUrl = 'foo/bar'; + const vm = createComponent(); + + vm.goToPreviousDirectoryClicked(prevUrl); + + expect(RepoService.url).toEqual(prevUrl); + }); + }); + }); }); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index f3572804b4a..d2a790ad73a 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -12,7 +12,6 @@ describe('RepoTab', () => { it('renders a close link and a name link', () => { const tab = { - loading: false, url: 'url', name: 'name', }; @@ -22,38 +21,21 @@ describe('RepoTab', () => { const close = vm.$el.querySelector('.close'); const name = vm.$el.querySelector(`a[title="${tab.url}"]`); - spyOn(vm, 'xClicked'); + spyOn(vm, 'closeTab'); spyOn(vm, 'tabClicked'); expect(close.querySelector('.fa-times')).toBeTruthy(); - expect(name.textContent).toEqual(tab.name); + expect(name.textContent.trim()).toEqual(tab.name); close.click(); name.click(); - expect(vm.xClicked).toHaveBeenCalledWith(tab); + expect(vm.closeTab).toHaveBeenCalledWith(tab); expect(vm.tabClicked).toHaveBeenCalledWith(tab); }); - it('renders a spinner if tab is loading', () => { - const tab = { - loading: true, - url: 'url', - }; - const vm = createComponent({ - tab, - }); - const close = vm.$el.querySelector('.close'); - const name = vm.$el.querySelector(`a[title="${tab.url}"]`); - - expect(close).toBeFalsy(); - expect(name).toBeFalsy(); - expect(vm.$el.querySelector('.fa.fa-spinner.fa-spin')).toBeTruthy(); - }); - it('renders an fa-circle icon if tab is changed', () => { const tab = { - loading: false, url: 'url', name: 'name', changed: true, @@ -66,22 +48,22 @@ describe('RepoTab', () => { }); describe('methods', () => { - describe('xClicked', () => { + describe('closeTab', () => { const vm = jasmine.createSpyObj('vm', ['$emit']); it('returns undefined and does not $emit if file is changed', () => { const file = { changed: true }; - const returnVal = repoTab.methods.xClicked.call(vm, file); + const returnVal = repoTab.methods.closeTab.call(vm, file); expect(returnVal).toBeUndefined(); expect(vm.$emit).not.toHaveBeenCalled(); }); - it('$emits xclicked event with file obj', () => { + it('$emits tabclosed event with file obj', () => { const file = { changed: false }; - repoTab.methods.xClicked.call(vm, file); + repoTab.methods.closeTab.call(vm, file); - expect(vm.$emit).toHaveBeenCalledWith('xclicked', file); + expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file); }); }); }); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index fdb12cfc00f..a02b54efafc 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -18,44 +18,25 @@ describe('RepoTabs', () => { it('renders a list of tabs', () => { RepoStore.openedFiles = openedFiles; - RepoStore.tabsOverflow = true; const vm = createComponent(); const tabs = [...vm.$el.querySelectorAll(':scope > li')]; expect(vm.$el.id).toEqual('tabs'); - expect(vm.$el.classList.contains('overflown')).toBeTruthy(); expect(tabs.length).toEqual(3); expect(tabs[0].classList.contains('active')).toBeTruthy(); expect(tabs[1].classList.contains('active')).toBeFalsy(); expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); }); - it('does not render a tabs list if not isMini', () => { - RepoStore.openedFiles = []; - - const vm = createComponent(); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); - - it('does not apply overflown class if not tabsOverflow', () => { - RepoStore.openedFiles = openedFiles; - RepoStore.tabsOverflow = false; - - const vm = createComponent(); - - expect(vm.$el.classList.contains('overflown')).toBeFalsy(); - }); - describe('methods', () => { - describe('xClicked', () => { + describe('tabClosed', () => { it('calls removeFromOpenedFiles with file obj', () => { const file = {}; spyOn(RepoStore, 'removeFromOpenedFiles'); - repoTabs.methods.xClicked(file); + repoTabs.methods.tabClosed(file); expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file); }); diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index 90eac1ed1ab..88a33caf2e3 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -41,7 +41,7 @@ describe('Confidential Issue Sidebar Block', () => { ).toBe(true); expect( - vm2.$el.innerHTML.includes('None'), + vm2.$el.innerHTML.includes('Not confidential'), ).toBe(true); }); diff --git a/spec/lib/after_commit_queue_spec.rb b/spec/lib/after_commit_queue_spec.rb new file mode 100644 index 00000000000..6e7c2ec2363 --- /dev/null +++ b/spec/lib/after_commit_queue_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe AfterCommitQueue do + it 'runs after transaction is committed' do + called = false + test_proc = proc { called = true } + + project = build(:project) + project.run_after_commit(&test_proc) + + project.save + + expect(called).to be true + end +end diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb index 2c972da682e..bdf10a5e2a2 100644 --- a/spec/lib/bitbucket/paginator_spec.rb +++ b/spec/lib/bitbucket/paginator_spec.rb @@ -15,7 +15,7 @@ describe Bitbucket::Paginator do expect(paginator.items).to match(['item_2']) allow(paginator).to receive(:fetch_next_page).and_return(nil) - expect{ paginator.items }.to raise_error(StopIteration) + expect { paginator.items }.to raise_error(StopIteration) end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ed571a2ba05..ee28387cd48 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1323,11 +1323,11 @@ EOT describe "Error handling" do it "fails to parse YAML" do - expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) + expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) end it "indicates that object is invalid" do - expect{GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError) + expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError) end it "returns errors if tags parameter is invalid" do diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index b0efcab47fb..87ae6b6cf01 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -5,7 +5,7 @@ describe EventFilter do let(:source_user) { create(:user) } let!(:public_project) { create(:project, :public) } - let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) } + let!(:push_event) { create(:push_event, project: public_project, author: source_user) } let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) } let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) } let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) } diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 867f7d55af7..e13406d1972 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -56,7 +56,7 @@ describe ExtractsPath do context 'subclass overrides get_id' do it 'uses ref returned by get_id' do - allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } + allow_any_instance_of(self.class).to receive(:get_id) { '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } assign_ref_vars diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb index 49501931dd2..c44bc1840df 100644 --- a/spec/lib/file_size_validator_spec.rb +++ b/spec/lib/file_size_validator_spec.rb @@ -24,13 +24,13 @@ describe FileSizeValidator do describe 'options uses a symbol' do let(:options) do { - maximum: :test, + maximum: :max_attachment_size, attributes: { attachment: attachment } } end before do - allow(note).to receive(:test) { 10 } + expect(note).to receive(:max_attachment_size) { 10 } end it 'attachment exceeds maximum limit' do diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 18843cbe992..7cd2ce82eda 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -123,6 +123,17 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do include_examples 'updated MR diff' end + context 'when the merge request diffs do not have too_large set' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } + + let(:diffs) do + expected_diffs.map { |diff| diff.except(:too_large) } + end + + include_examples 'updated MR diff' + end + context 'when the merge request diffs have binary content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:expected_diffs) { diffs } @@ -170,7 +181,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs are Rugged::Patch instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } - let(:diffs) { first_commit.diff_from_parent.patches } + let(:diffs) { first_commit.rugged_diff_from_parent.patches } let(:expected_diffs) { [] } include_examples 'updated MR diff' @@ -179,7 +190,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when the merge request diffs are Rugged::Diff::Delta instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } - let(:diffs) { first_commit.diff_from_parent.deltas } + let(:diffs) { first_commit.rugged_diff_from_parent.deltas } let(:expected_diffs) { [] } include_examples 'updated MR diff' diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb new file mode 100644 index 00000000000..0d5fffa38ff --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -0,0 +1,412 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event do + describe '#commit_title' do + it 'returns nil when there are no commits' do + expect(described_class.new.commit_title).to be_nil + end + + it 'returns nil when there are commits without commit messages' do + event = described_class.new + + allow(event).to receive(:commits).and_return([{ id: '123' }]) + + expect(event.commit_title).to be_nil + end + + it 'returns the commit message when it is less than 70 characters long' do + event = described_class.new + + allow(event).to receive(:commits).and_return([{ message: 'Hello world' }]) + + expect(event.commit_title).to eq('Hello world') + end + + it 'returns the first line of a commit message if multiple lines are present' do + event = described_class.new + + allow(event).to receive(:commits).and_return([{ message: "Hello\n\nworld" }]) + + expect(event.commit_title).to eq('Hello') + end + + it 'truncates the commit to 70 characters when it is too long' do + event = described_class.new + + allow(event).to receive(:commits).and_return([{ message: 'a' * 100 }]) + + expect(event.commit_title).to eq(('a' * 67) + '...') + end + end + + describe '#commit_from_sha' do + it 'returns nil when pushing to a new ref' do + event = described_class.new + + allow(event).to receive(:create?).and_return(true) + + expect(event.commit_from_sha).to be_nil + end + + it 'returns the ID of the first commit when pushing to an existing ref' do + event = described_class.new + + allow(event).to receive(:create?).and_return(false) + allow(event).to receive(:data).and_return(before: '123') + + expect(event.commit_from_sha).to eq('123') + end + end + + describe '#commit_to_sha' do + it 'returns nil when removing an existing ref' do + event = described_class.new + + allow(event).to receive(:remove?).and_return(true) + + expect(event.commit_to_sha).to be_nil + end + + it 'returns the ID of the last commit when pushing to an existing ref' do + event = described_class.new + + allow(event).to receive(:remove?).and_return(false) + allow(event).to receive(:data).and_return(after: '123') + + expect(event.commit_to_sha).to eq('123') + end + end + + describe '#data' do + it 'returns the deserialized data' do + event = described_class.new(data: { before: '123' }) + + expect(event.data).to eq(before: '123') + end + + it 'returns an empty hash when no data is present' do + event = described_class.new + + expect(event.data).to eq({}) + end + end + + describe '#commits' do + it 'returns an Array of commits' do + event = described_class.new(data: { commits: [{ id: '123' }] }) + + expect(event.commits).to eq([{ id: '123' }]) + end + + it 'returns an empty array when no data is present' do + event = described_class.new + + expect(event.commits).to eq([]) + end + end + + describe '#commit_count' do + it 'returns the number of commits' do + event = described_class.new(data: { total_commits_count: 2 }) + + expect(event.commit_count).to eq(2) + end + + it 'returns 0 when no data is present' do + event = described_class.new + + expect(event.commit_count).to eq(0) + end + end + + describe '#ref' do + it 'returns the name of the ref' do + event = described_class.new(data: { ref: 'refs/heads/master' }) + + expect(event.ref).to eq('refs/heads/master') + end + end + + describe '#trimmed_ref_name' do + it 'returns the trimmed ref name for a branch' do + event = described_class.new(data: { ref: 'refs/heads/master' }) + + expect(event.trimmed_ref_name).to eq('master') + end + + it 'returns the trimmed ref name for a tag' do + event = described_class.new(data: { ref: 'refs/tags/v1.2' }) + + expect(event.trimmed_ref_name).to eq('v1.2') + end + end + + describe '#create?' do + it 'returns true when creating a new ref' do + event = described_class.new(data: { before: described_class::BLANK_REF }) + + expect(event.create?).to eq(true) + end + + it 'returns false when pushing to an existing ref' do + event = described_class.new(data: { before: '123' }) + + expect(event.create?).to eq(false) + end + end + + describe '#remove?' do + it 'returns true when removing an existing ref' do + event = described_class.new(data: { after: described_class::BLANK_REF }) + + expect(event.remove?).to eq(true) + end + + it 'returns false when pushing to an existing ref' do + event = described_class.new(data: { after: '123' }) + + expect(event.remove?).to eq(false) + end + end + + describe '#push_action' do + let(:event) { described_class.new } + + it 'returns :created when creating a new ref' do + allow(event).to receive(:create?).and_return(true) + + expect(event.push_action).to eq(:created) + end + + it 'returns :removed when removing an existing ref' do + allow(event).to receive(:create?).and_return(false) + allow(event).to receive(:remove?).and_return(true) + + expect(event.push_action).to eq(:removed) + end + + it 'returns :pushed when pushing to an existing ref' do + allow(event).to receive(:create?).and_return(false) + allow(event).to receive(:remove?).and_return(false) + + expect(event.push_action).to eq(:pushed) + end + end + + describe '#ref_type' do + let(:event) { described_class.new } + + it 'returns :tag for a tag' do + allow(event).to receive(:ref).and_return('refs/tags/1.2') + + expect(event.ref_type).to eq(:tag) + end + + it 'returns :branch for a branch' do + allow(event).to receive(:ref).and_return('refs/heads/1.2') + + expect(event.ref_type).to eq(:branch) + end + end +end + +## +# The background migration relies on a temporary table, hence we're migrating +# to a specific version of the database where said table is still present. +# +describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170608152748 do + let(:migration) { described_class.new } + let(:project) { create(:project_empty_repo) } + let(:author) { create(:user) } + + # We can not rely on FactoryGirl as the state of Event may change in ways that + # the background migration does not expect, hence we use the Event class of + # the migration itself. + def create_push_event(project, author, data = nil) + klass = Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event + + klass.create!( + action: klass::PUSHED, + project_id: project.id, + author_id: author.id, + data: data + ) + end + + describe '#perform' do + it 'returns if data should not be migrated' do + allow(migration).to receive(:migrate?).and_return(false) + + expect(migration).not_to receive(:find_events) + + migration.perform(1, 10) + end + + it 'migrates the range of events if data is to be migrated' do + event1 = create_push_event(project, author, { commits: [] }) + event2 = create_push_event(project, author, { commits: [] }) + + allow(migration).to receive(:migrate?).and_return(true) + + expect(migration).to receive(:process_event).twice + + migration.perform(event1.id, event2.id) + end + end + + describe '#process_event' do + it 'processes a regular event' do + event = double(:event, push_event?: false) + + expect(migration).to receive(:replicate_event) + expect(migration).not_to receive(:create_push_event_payload) + + migration.process_event(event) + end + + it 'processes a push event' do + event = double(:event, push_event?: true) + + expect(migration).to receive(:replicate_event) + expect(migration).to receive(:create_push_event_payload) + + migration.process_event(event) + end + end + + describe '#replicate_event' do + it 'replicates the event to the "events_for_migration" table' do + event = create_push_event( + project, + author, + data: { commits: [] }, + title: 'bla' + ) + + attributes = event + .attributes.with_indifferent_access.except(:title, :data) + + expect(described_class::EventForMigration) + .to receive(:create!) + .with(attributes) + + migration.replicate_event(event) + end + end + + describe '#create_push_event_payload' do + let(:push_data) do + { + commits: [], + ref: 'refs/heads/master', + before: '156e0e9adc587a383a7eeb5b21ddecb9044768a8', + after: '0' * 40, + total_commits_count: 1 + } + end + + let(:event) do + create_push_event(project, author, push_data) + end + + before do + # The foreign key in push_event_payloads at this point points to the + # "events_for_migration" table so we need to make sure a row exists in + # said table. + migration.replicate_event(event) + end + + it 'creates a push event payload for an event' do + payload = migration.create_push_event_payload(event) + + expect(PushEventPayload.count).to eq(1) + expect(payload.valid?).to eq(true) + end + + it 'does not create push event payloads for removed events' do + allow(event).to receive(:id).and_return(-1) + + payload = migration.create_push_event_payload(event) + + expect(payload).to be_nil + expect(PushEventPayload.count).to eq(0) + end + + it 'encodes and decodes the commit IDs from and to binary data' do + payload = migration.create_push_event_payload(event) + packed = migration.pack(push_data[:before]) + + expect(payload.commit_from).to eq(packed) + expect(payload.commit_to).to be_nil + end + end + + describe '#find_events' do + it 'returns the events for the given ID range' do + event1 = create_push_event(project, author, { commits: [] }) + event2 = create_push_event(project, author, { commits: [] }) + event3 = create_push_event(project, author, { commits: [] }) + events = migration.find_events(event1.id, event2.id) + + expect(events.length).to eq(2) + expect(events.pluck(:id)).not_to include(event3.id) + end + end + + describe '#migrate?' do + it 'returns true when data should be migrated' do + allow(described_class::Event) + .to receive(:table_exists?).and_return(true) + + allow(described_class::PushEventPayload) + .to receive(:table_exists?).and_return(true) + + allow(described_class::EventForMigration) + .to receive(:table_exists?).and_return(true) + + expect(migration.migrate?).to eq(true) + end + + it 'returns false if the "events" table does not exist' do + allow(described_class::Event) + .to receive(:table_exists?).and_return(false) + + expect(migration.migrate?).to eq(false) + end + + it 'returns false if the "push_event_payloads" table does not exist' do + allow(described_class::Event) + .to receive(:table_exists?).and_return(true) + + allow(described_class::PushEventPayload) + .to receive(:table_exists?).and_return(false) + + expect(migration.migrate?).to eq(false) + end + + it 'returns false when the "events_for_migration" table does not exist' do + allow(described_class::Event) + .to receive(:table_exists?).and_return(true) + + allow(described_class::PushEventPayload) + .to receive(:table_exists?).and_return(true) + + allow(described_class::EventForMigration) + .to receive(:table_exists?).and_return(false) + + expect(migration.migrate?).to eq(false) + end + end + + describe '#pack' do + it 'packs a SHA1 into a 20 byte binary string' do + packed = migration.pack('156e0e9adc587a383a7eeb5b21ddecb9044768a8') + + expect(packed.bytesize).to eq(20) + end + + it 'returns nil if the input value is nil' do + expect(migration.pack(nil)).to be_nil + end + end +end diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb new file mode 100644 index 00000000000..ee60e498b59 --- /dev/null +++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') } + let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') } + let(:new_uploads_dir) { File.join('uploads', '-', 'system', 'personal_snippet') } + let(:snippet) do + snippet = create(:personal_snippet) + create_upload_for_snippet(snippet) + snippet.update_attributes!(description: markdown_linking_file(snippet)) + snippet + end + + let(:migration) { described_class.new } + + before do + allow(migration).to receive(:base_directory) { test_dir } + end + + describe '#perform' do + it 'moves the file on the disk' do + expected_path = File.join(test_dir, new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt') + + migration.perform(old_uploads_dir, new_uploads_dir) + + expect(File.exist?(expected_path)).to be_truthy + end + + it 'updates the markdown of the snippet' do + expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt') + expected_markdown = "[an upload](#{expected_path})" + + migration.perform(old_uploads_dir, new_uploads_dir) + + expect(snippet.reload.description).to eq(expected_markdown) + end + + it 'updates the markdown of notes' do + expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt') + expected_markdown = "with [an upload](#{expected_path})" + + note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown_linking_file(snippet)}") + + migration.perform(old_uploads_dir, new_uploads_dir) + + expect(note.reload.note).to eq(expected_markdown) + end + end + + def create_upload_for_snippet(snippet) + snippet_path = path_for_file_in_snippet(snippet) + path = File.join(old_uploads_dir, snippet.id.to_s, snippet_path) + absolute_path = File.join(test_dir, path) + + FileUtils.mkdir_p(File.dirname(absolute_path)) + FileUtils.touch(absolute_path) + + create(:upload, model: snippet, path: snippet_path, uploader: PersonalFileUploader) + end + + def path_for_file_in_snippet(snippet) + secret = "secret#{snippet.id}" + filename = 'upload.txt' + + File.join(secret, filename) + end + + def markdown_linking_file(snippet) + path = File.join(old_uploads_dir, snippet.id.to_s, path_for_file_in_snippet(snippet)) + "[an upload](#{path})" + end +end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index d7f7b26740d..ae5b31dc12d 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do has_wiki?: false) end - let(:namespace){ create(:group, owner: user) } + let(:namespace) { create(:group, owner: user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index 6c4cfa1203e..f8c8b83a3ac 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::Checks::ForcePush do let(:project) { create(:project, :repository) } - context "exit code checking" do + context "exit code checking", skip_gitaly_mock: true do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index ebe5af56160..e5555546fa8 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -295,7 +295,7 @@ describe Gitlab::Ci::Trace::Stream do end context 'malicious regexp' do - let(:data) { malicious_text } + let(:data) { malicious_text } let(:regex) { malicious_regexp } include_examples 'malicious regexp' diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index 854aaa34c73..0560c47f03f 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -6,10 +6,10 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do let(:user) { create(:user, :admin) } let(:start_time_attrs) { Issue.arel_table[:created_at] } let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] } - let(:options) do + let(:options) do { start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs, - from: 30.days.ago } + from: 30.days.ago } end subject do diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb index c519984a267..2bdb0dfc736 100644 --- a/spec/lib/gitlab/daemon_spec.rb +++ b/spec/lib/gitlab/daemon_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Daemon do allow(Kernel).to receive(:at_exit) end - after(:each) do + after do described_class.instance_variable_set(:@instance, nil) end diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb index 6415e4083d6..aaa42566a4d 100644 --- a/spec/lib/gitlab/data_builder/note_spec.rb +++ b/spec/lib/gitlab/data_builder/note_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::DataBuilder::Note do let(:data) { described_class.build(note, user) } let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors - before(:each) do + before do expect(data).to have_key(:object_attributes) expect(data[:object_attributes]).to have_key(:url) expect(data[:object_attributes][:url]) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index c5f9aecd867..5fa94999d25 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -51,6 +51,28 @@ describe Gitlab::Database do end end + describe '.join_lateral_supported?' do + it 'returns false when using MySQL' do + allow(described_class).to receive(:postgresql?).and_return(false) + + expect(described_class.join_lateral_supported?).to eq(false) + end + + it 'returns false when using PostgreSQL 9.2' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.2.1') + + expect(described_class.join_lateral_supported?).to eq(false) + end + + it 'returns true when using PostgreSQL 9.3.0 or newer' do + allow(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:version).and_return('9.3.0') + + expect(described_class.join_lateral_supported?).to eq(true) + end + end + describe '.nulls_last_order' do context 'when using PostgreSQL' do before do diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 1482ef7132d..8b14b227e65 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -30,6 +30,53 @@ describe Gitlab::EncodingHelper do it 'leaves binary string as is' do expect(ext_class.encode!(binary_string)).to eq(binary_string) end + + context 'with corrupted diff' do + let(:corrupted_diff) do + with_empty_bare_repository do |repo| + content = File.read(Rails.root.join( + 'spec/fixtures/encoding/Japanese.md').to_s) + commit_a = commit(repo, 'Japanese.md', content) + commit_b = commit(repo, 'Japanese.md', + content.sub('[TODO: Link]', '[現在作業中です: Link]')) + + repo.diff(commit_a, commit_b).each_line.map(&:content).join + end + end + + let(:cleaned_diff) do + corrupted_diff.dup.force_encoding('UTF-8') + .encode!('UTF-8', invalid: :replace, replace: '') + end + + let(:encoded_diff) do + described_class.encode!(corrupted_diff.dup) + end + + it 'does not corrupt data but remove invalid characters' do + expect(encoded_diff).to eq(cleaned_diff) + end + + def commit(repo, path, content) + oid = repo.write(content, :blob) + index = repo.index + + index.read_tree(repo.head.target.tree) unless repo.empty? + + index.add(path: path, oid: oid, mode: 0100644) + user = { name: 'Test', email: 'test@example.com' } + + Rugged::Commit.create( + repo, + tree: index.write_tree(repo), + author: user, + committer: user, + message: "Update #{path}", + parents: repo.empty? ? [] : [repo.head.target].compact, + update_ref: 'HEAD' + ) + end + end end describe '#encode_utf8' do diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 26d7a364f5b..ac33cd8a2c9 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Git::Commit, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } - let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } + let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } let(:rugged_commit) do repository.rugged.lookup(SeedRepo::Commit::ID) end @@ -24,7 +24,7 @@ describe Gitlab::Git::Commit, seed_helper: true do } @parents = [repo.head.target] - @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) } + @gitlab_parents = @parents.map { |c| described_class.decorate(repository, c) } @tree = @parents.first.tree sha = Rugged::Commit.create( @@ -38,7 +38,7 @@ describe Gitlab::Git::Commit, seed_helper: true do ) @raw_commit = repo.lookup(sha) - @commit = Gitlab::Git::Commit.new(@raw_commit) + @commit = described_class.new(repository, @raw_commit) end it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) } @@ -66,6 +66,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe "Commit info from gitaly commit" do let(:id) { 'f00' } + let(:parent_ids) { %w(b45 b46) } let(:subject) { "My commit".force_encoding('ASCII-8BIT') } let(:body) { subject + "My body".force_encoding('ASCII-8BIT') } let(:committer) do @@ -88,10 +89,11 @@ describe Gitlab::Git::Commit, seed_helper: true do subject: subject, body: body, author: author, - committer: committer + committer: committer, + parent_ids: parent_ids ) end - let(:commit) { described_class.new(Gitlab::GitalyClient::Commit.new(repository, gitaly_commit)) } + let(:commit) { described_class.new(repository, gitaly_commit) } it { expect(commit.short_id).to eq(id[0..10]) } it { expect(commit.id).to eq(id) } @@ -102,6 +104,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(commit.author_name).to eq(author.name) } it { expect(commit.committer_name).to eq(committer.name) } it { expect(commit.committer_email).to eq(committer.email) } + it { expect(commit.parent_ids).to eq(parent_ids) } context 'no body' do let(:body) { "".force_encoding('ASCII-8BIT') } @@ -113,45 +116,45 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'Class methods' do describe '.find' do it "should return first head commit if without params" do - expect(Gitlab::Git::Commit.last(repository).id).to eq( + expect(described_class.last(repository).id).to eq( repository.rugged.head.target.oid ) end it "should return valid commit" do - expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit + expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit end it "should return valid commit for tag" do - expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') end it "should return nil for non-commit ids" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") - expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil + expect(described_class.find(repository, blob.id)).to be_nil end it "should return nil for parent of non-commit object" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") - expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil + expect(described_class.find(repository, "#{blob.id}^")).to be_nil end it "should return nil for nonexisting ids" do - expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil + expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil end context 'with broken repo' do let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH) } it 'returns nil' do - expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil + expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil end end end describe '.last_for_path' do context 'no path' do - subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } + subject { described_class.last_for_path(repository, 'master') } describe '#id' do subject { super().id } @@ -160,7 +163,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'path' do - subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') } + subject { described_class.last_for_path(repository, 'master', 'files/ruby') } describe '#id' do subject { super().id } @@ -169,7 +172,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'ref + path' do - subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } + subject { described_class.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } describe '#id' do subject { super().id } @@ -181,7 +184,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.where' do context 'path is empty string' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'master', path: '', @@ -199,7 +202,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'path is nil' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'master', path: nil, @@ -217,7 +220,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref is branch name' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'master', path: 'files', @@ -237,7 +240,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref is commit id' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e", path: 'files', @@ -257,7 +260,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref is tag' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'v1.0.0', path: 'files', @@ -278,7 +281,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.between' do subject do - commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) + commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) commits.map { |c| c.id } end @@ -294,12 +297,12 @@ describe Gitlab::Git::Commit, seed_helper: true do it 'should return a return a collection of commits' do commits = described_class.find_all(repository) - expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) ) + expect(commits).to all( be_a_kind_of(described_class) ) end context 'max_count' do subject do - commits = Gitlab::Git::Commit.find_all( + commits = described_class.find_all( repository, max_count: 50 ) @@ -307,8 +310,8 @@ describe Gitlab::Git::Commit, seed_helper: true do commits.map(&:id) end - it 'has 33 elements' do - expect(subject.size).to eq(33) + it 'has 34 elements' do + expect(subject.size).to eq(34) end it 'includes the expected commits' do @@ -322,7 +325,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref + max_count + skip' do subject do - commits = Gitlab::Git::Commit.find_all( + commits = described_class.find_all( repository, ref: 'master', max_count: 50, @@ -374,7 +377,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '#init_from_rugged' do - let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) } + let(:gitlab_commit) { described_class.new(repository, rugged_commit) } subject { gitlab_commit } describe '#id' do @@ -384,7 +387,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '#init_from_hash' do - let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } + let(:commit) { described_class.new(repository, sample_commit_hash) } subject { commit } describe '#id' do @@ -451,7 +454,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '#ref_names' do - let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } + let(:commit) { described_class.find(repository, 'master') } subject { commit.ref_names(repository) } it 'has 1 element' do diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 0cfb210e390..3494f0cc98d 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -384,7 +384,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do context 'when go over safe limits on files' do let(:iterator) { [fake_diff(1, 1)] * 4 } - before(:each) do + before do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines }) end @@ -409,7 +409,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do ] end - before(:each) do + before do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) end @@ -434,7 +434,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do ] end - before(:each) do + before do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 20d6b58d6d1..4ef5d9070a2 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -289,7 +289,13 @@ describe Gitlab::Git::Repository, seed_helper: true do it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } end - context 'no submodules at commit' do + context 'no .gitmodules at commit' do + let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' } + + it { expect(submodule_url('six')).to eq(nil) } + end + + context 'no gitlink entry' do let(:ref) { '6d39438' } it { expect(submodule_url('six')).to eq(nil) } @@ -422,11 +428,11 @@ describe Gitlab::Git::Repository, seed_helper: true do it "should fail if we create an existing branch" do @repo.create_branch('duplicated_branch', 'master') - expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + expect {@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") end it "should fail if we create a branch from a non existing ref" do - expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + expect {@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") end after(:all) do @@ -505,17 +511,22 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#log" do - commit_with_old_name = nil - commit_with_new_name = nil - rename_commit = nil + let(:commit_with_old_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) + end + let(:commit_with_new_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) + end + let(:rename_commit) do + Gitlab::Git::Commit.decorate(repository, @rename_commit_id) + end before(:context) do # Add new commits so that there's a renamed file in the commit history repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged - - commit_with_old_name = Gitlab::Git::Commit.decorate(new_commit_edit_old_file(repo)) - rename_commit = Gitlab::Git::Commit.decorate(new_commit_move_file(repo)) - commit_with_new_name = Gitlab::Git::Commit.decorate(new_commit_edit_new_file(repo)) + @commit_with_old_name_id = new_commit_edit_old_file(repo) + @rename_commit_id = new_commit_move_file(repo) + @commit_with_new_name_id = new_commit_edit_new_file(repo) end after(:context) do @@ -754,7 +765,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } def commit_files(commit) - commit.diff_from_parent.deltas.flat_map do |delta| + commit.rugged_diff_from_parent.deltas.flat_map do |delta| [delta.old_file[:path], delta.new_file[:path]].uniq.compact end end @@ -981,7 +992,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to eq(9) + expect(repository.branch_count).to eq(10) end end @@ -997,7 +1008,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(master_file_paths).to include("files/html/500.html") end - it "dose not read submodule directory and empty directory of master branch" do + it "does not read submodule directory and empty directory of master branch" do expect(master_file_paths).not_to include("six") end @@ -1018,7 +1029,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end context "with no .gitattrbutes" do - before(:each) do + before do repository.copy_gitattributes("master") end @@ -1026,13 +1037,13 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(File.exist?(attributes_path)).to be_falsey end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end context "with .gitattrbutes" do - before(:each) do + before do repository.copy_gitattributes("gitattributes") end @@ -1045,13 +1056,13 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(contents).to eq("*.md binary\n") end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end context "with updated .gitattrbutes" do - before(:each) do + before do repository.copy_gitattributes("gitattributes") repository.copy_gitattributes("gitattributes-updated") end @@ -1065,13 +1076,13 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(contents).to eq("*.txt binary\n") end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end context "with no .gitattrbutes in HEAD but with previous info/attributes" do - before(:each) do + before do repository.copy_gitattributes("gitattributes") repository.copy_gitattributes("master") end @@ -1080,7 +1091,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(File.exist?(attributes_path)).to be_falsey end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index b2886628601..c86353abb7c 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -1,9 +1,30 @@ require 'spec_helper' describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do - let(:circuit_breaker) { described_class.new('default') } + let(:storage_name) { 'default' } + let(:circuit_breaker) { described_class.new(storage_name) } let(:hostname) { Gitlab::Environment.hostname } - let(:cache_key) { "storage_accessible:default:#{hostname}" } + let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } + + before do + # Override test-settings for the circuitbreaker with something more realistic + # for these specs. + stub_storage_settings('default' => { + 'path' => TestEnv.repos_path, + 'failure_count_threshold' => 10, + 'failure_wait_time' => 30, + 'failure_reset_time' => 1800, + 'storage_timeout' => 5 + }, + 'broken' => { + 'path' => 'tmp/tests/non-existent-repositories', + 'failure_count_threshold' => 10, + 'failure_wait_time' => 30, + 'failure_reset_time' => 1800, + 'storage_timeout' => 5 + } + ) + end def value_from_redis(name) Gitlab::Git::Storage.redis.with do |redis| @@ -96,14 +117,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end describe '#circuit_broken?' do - it 'is closed when there is no last failure' do + it 'is working when there is no last failure' do set_in_redis(:last_failure, nil) set_in_redis(:failure_count, 0) expect(circuit_breaker.circuit_broken?).to be_falsey end - it 'is open when there was a recent failure' do + it 'is broken when there was a recent failure' do Timecop.freeze do set_in_redis(:last_failure, 1.second.ago.to_f) set_in_redis(:failure_count, 1) @@ -112,16 +133,34 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - it 'is open when there are to many failures' do + it 'is broken when there are too many failures' do set_in_redis(:last_failure, 1.day.ago.to_f) set_in_redis(:failure_count, 200) expect(circuit_breaker.circuit_broken?).to be_truthy end + + context 'the `failure_wait_time` is set to 0' do + before do + stub_storage_settings('default' => { + 'failure_wait_time' => 0, + 'path' => TestEnv.repos_path + }) + end + + it 'is working even when there is a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 0.seconds.ago.to_f) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.circuit_broken?).to be_falsey + end + end + end end describe "storage_available?" do - context 'when the storage is available' do + context 'the storage is available' do it 'tracks that the storage was accessible an raises the error' do expect(circuit_breaker).to receive(:track_storage_accessible) @@ -136,8 +175,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - context 'when storage is not available' do - let(:circuit_breaker) { described_class.new('broken') } + context 'storage is not available' do + let(:storage_name) { 'broken' } it 'tracks that the storage was inaccessible' do expect(circuit_breaker).to receive(:track_storage_inaccessible) @@ -158,8 +197,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - context 'when the storage is not available' do - let(:circuit_breaker) { described_class.new('broken') } + context 'the storage is not available' do + let(:storage_name) { 'broken' } it 'raises an error' do expect(circuit_breaker).to receive(:track_storage_inaccessible) @@ -174,12 +213,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end describe '#track_storage_inaccessible' do - around(:each) do |example| - Timecop.freeze - - example.run - - Timecop.return + around do |example| + Timecop.freeze { example.run } end it 'records the failure time in redis' do diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index d71e0f84c65..7fe698fcb18 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::GitalyClient::CommitService do context 'when a commit does not have a parent' do it 'sends an RPC request with empty tree ref as left commit' do - initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863').raw request = Gitaly::CommitDiffRequest.new( repository: repository_message, left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', @@ -46,18 +46,10 @@ describe Gitlab::GitalyClient::CommitService do end end - it 'returns a Gitlab::Git::DiffCollection' do + it 'returns a Gitlab::GitalyClient::DiffStitcher' do ret = client.diff_from_parent(commit) - expect(ret).to be_kind_of(Gitlab::Git::DiffCollection) - end - - it 'passes options to Gitlab::Git::DiffCollection' do - options = { max_files: 31, max_lines: 13, from_gitaly: true } - - expect(Gitlab::Git::DiffCollection).to receive(:new).with(kind_of(Enumerable), options) - - client.diff_from_parent(commit, options) + expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher) end end @@ -120,4 +112,18 @@ describe Gitlab::GitalyClient::CommitService do client.tree_entries(repository, revision, path) end end + + describe '#find_commit' do + let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' } + it 'sends an RPC request' do + request = Gitaly::FindCommitRequest.new( + repository: repository_message, revision: revision + ) + + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit) + .with(request, kind_of(Hash)).and_return(double(commit: nil)) + + described_class.new(repository).find_commit(revision) + end + end end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb deleted file mode 100644 index 5c9c4ed1d7c..00000000000 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe Gitlab::GitalyClient::RepositoryService do - set(:project) { create(:project) } - let(:storage_name) { project.repository_storage } - let(:relative_path) { project.path_with_namespace + '.git' } - let(:client) { described_class.new(project.repository) } - - describe '#exists?' do - it 'sends an exists message' do - expect_any_instance_of(Gitaly::RepositoryService::Stub) - .to receive(:exists) - .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) - .and_call_original - - client.exists? - end - end -end diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb index da48d8f0670..82548c7fd31 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do owner: { name: "john" } }.with_indifferent_access end - let(:namespace){ create(:group, owner: user) } + let(:namespace) { create(:group, owner: user) } let(:token) { "asdffg" } let(:access_params) { { gitlab_access_token: token } } diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb index aad53938d52..8d5b60d50de 100644 --- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do "repositoryUrls" => ["https://vim.googlecode.com/git/"] ) end - let(:namespace){ create(:group, owner: user) } + let(:namespace) { create(:group, owner: user) } before do namespace.add_owner(user) diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index ddb8dd9f0f4..e521fcc6dc1 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -1,13 +1,13 @@ require 'rails_helper' -RSpec.describe Gitlab::Gpg::Commit do +describe Gitlab::Gpg::Commit do describe '#signature' do let!(:project) { create :project, :repository, path: 'sample-project' } let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - context 'unisgned commit' do + context 'unsigned commit' do it 'returns nil' do - expect(described_class.new(project.commit).signature).to be_nil + expect(described_class.new(project, commit_sha).signature).to be_nil end end @@ -16,18 +16,19 @@ RSpec.describe Gitlab::Gpg::Commit do create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first) end - let!(:commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - allow(raw_commit).to receive :save! - - create :commit, git_commit: raw_commit, project: project + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end it 'returns a valid signature' do - expect(described_class.new(commit).signature).to have_attributes( + expect(described_class.new(project, commit_sha).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: gpg_key, @@ -39,7 +40,7 @@ RSpec.describe Gitlab::Gpg::Commit do end it 'returns the cached signature on second call' do - gpg_commit = described_class.new(commit) + gpg_commit = described_class.new(project, commit_sha) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature @@ -53,18 +54,19 @@ RSpec.describe Gitlab::Gpg::Commit do context 'known but unverified public key' do let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key } - let!(:commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - allow(raw_commit).to receive :save! - - create :commit, git_commit: raw_commit, project: project + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end it 'returns an invalid signature' do - expect(described_class.new(commit).signature).to have_attributes( + expect(described_class.new(project, commit_sha).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: gpg_key, @@ -76,7 +78,7 @@ RSpec.describe Gitlab::Gpg::Commit do end it 'returns the cached signature on second call' do - gpg_commit = described_class.new(commit) + gpg_commit = described_class.new(project, commit_sha) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature @@ -88,20 +90,19 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'unknown public key' do - let!(:commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - allow(raw_commit).to receive :save! - - create :commit, - git_commit: raw_commit, - project: project + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end it 'returns an invalid signature' do - expect(described_class.new(commit).signature).to have_attributes( + expect(described_class.new(project, commit_sha).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: nil, @@ -113,7 +114,7 @@ RSpec.describe Gitlab::Gpg::Commit do end it 'returns the cached signature on second call' do - gpg_commit = described_class.new(commit) + gpg_commit = described_class.new(project, commit_sha) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index c4e04ee46a2..4de4419de27 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -4,23 +4,16 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } let!(:project) { create :project, :repository, path: 'sample-project' } - let!(:raw_commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - - allow(raw_commit).to receive :save! - - raw_commit - end - - let!(:commit) do - create :commit, git_commit: raw_commit, project: project - end before do - allow_any_instance_of(Project).to receive(:commit).and_return(commit) + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end context 'gpg signature did have an associated gpg key which was removed later' do diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 8041518117d..30ad033b204 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -43,6 +43,58 @@ describe Gitlab::Gpg do ).to eq [] end end + + describe '.current_home_dir' do + let(:default_home_dir) { GPGME::Engine.dirinfo('homedir') } + + it 'returns the default value when no explicit home dir has been set' do + expect(described_class.current_home_dir).to eq default_home_dir + end + + it 'returns the explicitely set home dir' do + GPGME::Engine.home_dir = '/tmp/gpg' + + expect(described_class.current_home_dir).to eq '/tmp/gpg' + + GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir') + end + + it 'returns the default value when explicitely setting the home dir to nil' do + GPGME::Engine.home_dir = nil + + expect(described_class.current_home_dir).to eq default_home_dir + end + end + + describe '.using_tmp_keychain' do + it "the second thread does not change the first thread's directory" do + thread1 = Thread.new do + described_class.using_tmp_keychain do + dir = described_class.current_home_dir + sleep 0.1 + expect(described_class.current_home_dir).to eq dir + end + end + + thread2 = Thread.new do + described_class.using_tmp_keychain do + sleep 0.2 + end + end + + thread1.join + thread2.join + end + + it 'allows recursive execution in the same thread' do + expect do + described_class.using_tmp_keychain do + described_class.using_tmp_keychain do + end + end + end.not_to raise_error(ThreadError) + end + end end describe Gitlab::Gpg::CurrentKeyChain do diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 26574df8bb5..f5c9680bf59 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -106,12 +106,6 @@ describe Gitlab::HealthChecks::FsShardsCheck do }.with_indifferent_access end - # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128 - around(:each) do |example| # rubocop:disable RSpec/AroundBlock - times_to_try = ENV['CI'] ? 4 : 1 - example.run_with_retry retry: times_to_try - end - it 'provides metrics' do metrics = described_class.metrics diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 6a41afe0c25..8da02b0cf00 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -22,6 +22,7 @@ events: - author - project - target +- push_event_payload notes: - award_emoji - project @@ -272,3 +273,5 @@ timelogs: - issue - merge_request - user +push_event_payload: +- event diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index 574748756bd..cd5a1b2982b 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::AttributeCleaner do - let(:relation_class){ double('relation_class').as_null_object } + let(:relation_class) { double('relation_class').as_null_object } let(:unsafe_hash) do { 'id' => 101, diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 690c7625c52..162b776e107 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::ImportExport::FileImporter do let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" } let(:valid_file) { "#{shared.export_path}/valid.json" } let(:symlink_file) { "#{shared.export_path}/invalid.json" } + let(:hidden_symlink_file) { "#{shared.export_path}/.hidden" } let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } before do @@ -25,6 +26,10 @@ describe Gitlab::ImportExport::FileImporter do expect(File.exist?(symlink_file)).to be false end + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + it 'removes symlinks in subfolders' do expect(File.exist?(subfolder_symlink_file)).to be false end diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index a78836c3c34..2d8f3d4a566 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -2,6 +2,20 @@ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "visibility_level": 10, "archived": false, + "milestones": [ + { + "id": 1, + "title": "test milestone", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + } + ], "labels": [ { "id": 2, @@ -14,20 +28,6 @@ "description": "", "type": "ProjectLabel", "priorities": [ - ] - }, - { - "id": 3, - "title": "test3", - "color": "#428bca", - "group_id": 8, - "created_at": "2016-07-22T08:55:44.161Z", - "updated_at": "2016-07-22T08:55:44.161Z", - "template": false, - "description": "", - "project_id": null, - "type": "GroupLabel", - "priorities": [ { "id": 1, "project_id": 5, @@ -39,10 +39,80 @@ ] } ], + "issues": [ + { + "id": 1, + "title": "Fugiat est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 20, + "updated_by_id": 1, + "confidential": false, + "deleted_at": null, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "label_links": [ + { + "id": 11, + "label_id": 6, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 6, + "title": "group label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "GroupLabel", + "priorities": [] + } + }, + { + "id": 11, + "label_id": 2, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 6, + "title": "project label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "ProjectLabel", + "priorities": [] + } + } + ] + } + ], "snippets": [ ], "hooks": [ ] -}
\ No newline at end of file +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 7ee0e22f28d..956f1d56eb4 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -183,7 +183,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do let(:restored_project_json) { project_tree_restorer.restore } before do - allow(ImportExport).to receive(:project_filename).and_return('project.light.json') + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') end @@ -195,7 +196,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do restored_project_json - expect(shared.errors.first).not_to include('test') + expect(shared.errors.first).to be_nil end end end @@ -219,15 +220,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end before do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + restored_project_json end - it 'has group labels' do - expect(GroupLabel.count).to eq(1) + it 'correctly restores project' do + expect(restored_project_json).to be_truthy + expect(shared.errors).to be_empty + end + + it 'has labels' do + expect(project.labels.count).to eq(2) + end + + it 'creates group label' do + expect(project.group.labels.count).to eq(1) end it 'has label priorities' do - expect(GroupLabel.first.priorities).not_to be_empty + expect(project.labels.first.priorities).not_to be_empty + end + + it 'has milestones' do + expect(project.milestones.count).to eq(1) + end + + it 'has issue' do + expect(project.issues.count).to eq(1) + expect(project.issues.first.labels.count).to eq(2) + end + + it 'has issue with group label and project label' do + labels = project.issues.first.labels + + expect(labels.where(type: "GroupLabel").count).to eq(1) + expect(labels.where(type: "ProjectLabel").count).to eq(1) end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 4dce48f8079..ae3b0173160 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -36,6 +36,14 @@ Event: - updated_at - action - author_id +PushEventPayload: +- commit_count +- action +- ref_type +- commit_from +- commit_to +- ref +- commit_title Note: - id - note diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb index f5fd5a96bc9..d643dc5342d 100644 --- a/spec/lib/gitlab/key_fingerprint_spec.rb +++ b/spec/lib/gitlab/key_fingerprint_spec.rb @@ -30,8 +30,8 @@ describe Gitlab::KeyFingerprint, lib: true do MD5_FINGERPRINTS = { rsa: '06:b2:8a:92:df:0e:11:2c:ca:7b:8f:a4:ba:6e:4b:fd', - ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e', - ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', + ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e', + ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', dss: '57:98:86:02:5f:9c:f4:9b:ad:5a:1e:51:92:0e:fd:2b' }.freeze diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index 57a91193004..8370adf9211 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -4,8 +4,8 @@ describe Gitlab::LDAP::AuthHash do let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - uid: '123456', - provider: 'ldapmain', + uid: '123456', + provider: 'ldapmain', info: info, extra: { raw_info: raw_info @@ -33,11 +33,11 @@ describe Gitlab::LDAP::AuthHash do context "without overridden attributes" do it "has the correct username" do - expect(auth_hash.username).to eq("123456") + expect(auth_hash.username).to eq("123456") end it "has the correct name" do - expect(auth_hash.name).to eq("Smith, J.") + expect(auth_hash.name).to eq("Smith, J.") end end @@ -54,11 +54,11 @@ describe Gitlab::LDAP::AuthHash do end it "has the correct username" do - expect(auth_hash.username).to eq("johnsmith@example.com") + expect(auth_hash.username).to eq("johnsmith@example.com") end it "has the correct name" do - expect(auth_hash.name).to eq("John Smith") + expect(auth_hash.name).to eq("John Smith") end end end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 292ec064a67..ca2213cd112 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::LDAP::Config do describe '#initialize' do it 'requires a provider' do - expect{ described_class.new }.to raise_error ArgumentError + expect { described_class.new }.to raise_error ArgumentError end it 'works' do @@ -15,7 +15,7 @@ describe Gitlab::LDAP::Config do end it 'raises an error if a unknown provider is used' do - expect{ described_class.new 'unknown' }.to raise_error(RuntimeError) + expect { described_class.new 'unknown' }.to raise_error(RuntimeError) end end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 175ceec44d7..5100a5a609e 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -61,12 +61,12 @@ describe Gitlab::LDAP::User do it "finds the user if already existing" do create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') - expect{ ldap_user.save }.not_to change{ User.count } + expect { ldap_user.save }.not_to change { User.count } end it "connects to existing non-ldap user if the email matches" do existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter") - expect{ ldap_user.save }.not_to change{ User.count } + expect { ldap_user.save }.not_to change { User.count } existing_user.reload expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' @@ -75,7 +75,7 @@ describe Gitlab::LDAP::User do it 'connects to existing ldap user if the extern_uid changes' do existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain') - expect{ ldap_user.save }.not_to change{ User.count } + expect { ldap_user.save }.not_to change { User.count } existing_user.reload expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' @@ -85,7 +85,7 @@ describe Gitlab::LDAP::User do it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain') - expect{ ldap_user_upper_case.save }.not_to change{ User.count } + expect { ldap_user_upper_case.save }.not_to change { User.count } existing_user.reload expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' @@ -106,7 +106,7 @@ describe Gitlab::LDAP::User do end it "creates a new user if not found" do - expect{ ldap_user.save }.to change{ User.count }.by(1) + expect { ldap_user.save }.to change { User.count }.by(1) end context 'when signup is disabled' do diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 461b1e4182a..ebe66948a91 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -4,10 +4,6 @@ describe Gitlab::Metrics::RequestsRackMiddleware do let(:app) { double('app') } subject { described_class.new(app) } - around do |example| - Timecop.freeze { example.run } - end - describe '#call' do let(:status) { 100 } let(:env) { { 'REQUEST_METHOD' => 'GET' } } @@ -28,16 +24,14 @@ describe Gitlab::Metrics::RequestsRackMiddleware do subject.call(env) end - it 'measures execution time' do - execution_time = 10 - allow(app).to receive(:call) do |*args| - Timecop.freeze(execution_time.seconds) - [200, nil, nil] - end + RSpec::Matchers.define :a_positive_execution_time do + match { |actual| actual > 0 } + end - expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: 200, method: 'get' }, execution_time) + it 'measures execution time' do + expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: 200, method: 'get' }, a_positive_execution_time) - subject.call(env) + Timecop.scale(3600) { subject.call(env) } end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 6c84c4a0c1e..2cf0f7516de 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -147,7 +147,7 @@ describe Gitlab::OAuth::User do end it 'throws an error' do - expect{ oauth_user.save }.to raise_error StandardError + expect { oauth_user.save }.to raise_error StandardError end end @@ -157,7 +157,7 @@ describe Gitlab::OAuth::User do end it 'throws an error' do - expect{ oauth_user.save }.to raise_error StandardError + expect { oauth_user.save }.to raise_error StandardError end end end @@ -481,7 +481,7 @@ describe Gitlab::OAuth::User do email: 'admin@othermail.com' } end - + it 'generates the username with a counter' do expect(gl_user.username).to eq('admin1') end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 12e75cdd5d0..d19bd611919 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::ProjectTemplate do describe '.all' do it 'returns a all templates' do expected = [ - described_class.new('rails', 'Ruby on Rails') + described_class.new('rails', 'Ruby on Rails'), + described_class.new('spring', 'Spring'), + described_class.new('express', 'NodeJS Express') ] expect(described_class.all).to be_an(Array) diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb index d7df4e35c31..5589db92b1d 100644 --- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do queries: [{ query_range: 'query_range_empty' }] - group: group_b priority: 1 - metrics: + metrics: - title: title required_metrics: ['metric_a'] weight: 1 @@ -148,7 +148,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do - group: group_a priority: 1 metrics: - - title: + - title: required_metrics: [] weight: 1 queries: [] diff --git a/spec/lib/gitlab/redis/wrapper_spec.rb b/spec/lib/gitlab/redis/wrapper_spec.rb index e1becd0a614..0c22a0d62cc 100644 --- a/spec/lib/gitlab/redis/wrapper_spec.rb +++ b/spec/lib/gitlab/redis/wrapper_spec.rb @@ -17,4 +17,11 @@ describe Gitlab::Redis::Wrapper do let(:class_redis_url) { Gitlab::Redis::Wrapper::DEFAULT_REDIS_URL } include_examples "redis_shared_examples" + + describe '.config_file_path' do + it 'returns the absolute path to the configuration file' do + expect(described_class.config_file_path('foo.yml')) + .to eq Rails.root.join('config', 'foo.yml').to_s + end + end end diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index e272bdb9284..8a28ad0e597 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::RequestContext do let(:env) { Hash.new } context 'when RequestStore::Middleware is used' do - around(:each) do |example| + around do |example| RequestStore::Middleware.new(-> (env) { example.run }).call({}) end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index 2827a18515e..19710029224 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -109,7 +109,7 @@ describe Gitlab::Saml::User do end it 'does not throw an error' do - expect{ saml_user.save }.not_to raise_error + expect { saml_user.save }.not_to raise_error end end @@ -119,7 +119,7 @@ describe Gitlab::Saml::User do end it 'throws an error' do - expect{ saml_user.save }.to raise_error StandardError + expect { saml_user.save }.to raise_error StandardError end end end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 2345874cf10..cfadee0bcf5 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -94,28 +94,41 @@ describe Gitlab::Shell do end describe 'projects commands' do - let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' } + let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') } + let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') } + let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') } before do - allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-projects-test') + allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) + allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path) allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end describe '#add_repository' do - it 'returns true when the command succeeds' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'add-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return([nil, 0]) + it 'creates a repository' do + created_path = File.join(TestEnv.repos_path, 'project', 'path.git') + hooks_path = File.join(created_path, 'hooks') + + begin + result = gitlab_shell.add_repository(TestEnv.repos_path, 'project/path') + + repo_stat = File.stat(created_path) rescue nil + hooks_stat = File.lstat(hooks_path) rescue nil + hooks_dir = File.realpath(hooks_path) + ensure + FileUtils.rm_rf(created_path) + end - expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be true + expect(result).to be_truthy + expect(repo_stat.mode & 0o777).to eq(0o770) + expect(hooks_stat.symlink?).to be_truthy + expect(hooks_dir).to eq(gitlab_shell_hooks_path) end it 'returns false when the command fails' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'add-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return(["error", 1]) + expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST) - expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be false + expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 5346881444d..baf8f6644bf 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::SQL::Union do empty_relation = User.none union = described_class.new([empty_relation, relation_1, relation_2]) - expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error + expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end end diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 6e0b1075a89..7098499f996 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -1,41 +1,28 @@ require 'spec_helper' describe Gitlab::Template::IssueTemplate do - subject { described_class } - - let(:user) { create(:user) } - - let(:project) do - create(:project, - :repository, - create_template: { - user: user, - access: Gitlab::Access::MASTER, - path: 'issue_templates' - }) - end + let(:project) { create(:project, :repository, create_templates: :issue) } describe '.all' do it 'strips the md suffix' do - expect(subject.all(project).first.name).not_to end_with('.issue_template') + expect(described_class.all(project).first.name).not_to end_with('.issue_template') end it 'combines the globals and rest' do - all = subject.all(project).map(&:name) + all = described_class.all(project).map(&:name) expect(all).to include('bug') expect(all).to include('feature_proposal') - expect(all).to include('template_test') end end describe '.find' do it 'returns nil if the file does not exist' do - expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) + expect { described_class.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) end it 'returns the issue object of a valid file' do - ruby = subject.find('bug', project) + ruby = described_class.find('bug', project) expect(ruby).to be_a described_class expect(ruby.name).to eq('bug') @@ -44,21 +31,17 @@ describe Gitlab::Template::IssueTemplate do describe '.by_category' do it 'return array of templates' do - all = subject.by_category('', project).map(&:name) + all = described_class.by_category('', project).map(&:name) expect(all).to include('bug') expect(all).to include('feature_proposal') - expect(all).to include('template_test') end context 'when repo is bare or empty' do let(:empty_project) { create(:project) } - before do - empty_project.add_user(user, Gitlab::Access::MASTER) - end - it "returns empty array" do - templates = subject.by_category('', empty_project) + templates = described_class.by_category('', empty_project) + expect(templates).to be_empty end end @@ -66,26 +49,23 @@ describe Gitlab::Template::IssueTemplate do describe '#content' do it 'loads the full file' do - issue_template = subject.new('.gitlab/issue_templates/bug.md', project) + issue_template = described_class.new('.gitlab/issue_templates/bug.md', project) expect(issue_template.name).to eq 'bug' expect(issue_template.content).to eq('something valid') end it 'raises error when file is not found' do - issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project) + issue_template = described_class.new('.gitlab/issue_templates/bugnot.md', project) expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) end context "when repo is empty" do let(:empty_project) { create(:project) } - before do - empty_project.add_user(user, Gitlab::Access::MASTER) - end - it "raises file not found" do - issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project) + issue_template = described_class.new('.gitlab/issue_templates/not_existent.md', empty_project) + expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) end end diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index b952274cd24..bd7ff64aa8a 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -1,41 +1,28 @@ require 'spec_helper' describe Gitlab::Template::MergeRequestTemplate do - subject { described_class } - - let(:user) { create(:user) } - - let(:project) do - create(:project, - :repository, - create_template: { - user: user, - access: Gitlab::Access::MASTER, - path: 'merge_request_templates' - }) - end + let(:project) { create(:project, :repository, create_templates: :merge_request) } describe '.all' do it 'strips the md suffix' do - expect(subject.all(project).first.name).not_to end_with('.issue_template') + expect(described_class.all(project).first.name).not_to end_with('.issue_template') end it 'combines the globals and rest' do - all = subject.all(project).map(&:name) + all = described_class.all(project).map(&:name) expect(all).to include('bug') expect(all).to include('feature_proposal') - expect(all).to include('template_test') end end describe '.find' do it 'returns nil if the file does not exist' do - expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) + expect { described_class.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) end it 'returns the merge request object of a valid file' do - ruby = subject.find('bug', project) + ruby = described_class.find('bug', project) expect(ruby).to be_a described_class expect(ruby.name).to eq('bug') @@ -44,21 +31,17 @@ describe Gitlab::Template::MergeRequestTemplate do describe '.by_category' do it 'return array of templates' do - all = subject.by_category('', project).map(&:name) + all = described_class.by_category('', project).map(&:name) expect(all).to include('bug') expect(all).to include('feature_proposal') - expect(all).to include('template_test') end context 'when repo is bare or empty' do let(:empty_project) { create(:project) } - before do - empty_project.add_user(user, Gitlab::Access::MASTER) - end - it "returns empty array" do - templates = subject.by_category('', empty_project) + templates = described_class.by_category('', empty_project) + expect(templates).to be_empty end end @@ -66,26 +49,23 @@ describe Gitlab::Template::MergeRequestTemplate do describe '#content' do it 'loads the full file' do - issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project) + issue_template = described_class.new('.gitlab/merge_request_templates/bug.md', project) expect(issue_template.name).to eq 'bug' expect(issue_template.content).to eq('something valid') end it 'raises error when file is not found' do - issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project) + issue_template = described_class.new('.gitlab/merge_request_templates/bugnot.md', project) expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) end context "when repo is empty" do let(:empty_project) { create(:project) } - before do - empty_project.add_user(user, Gitlab::Access::MASTER) - end - it "raises file not found" do - issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project) + issue_template = described_class.new('.gitlab/merge_request_templates/not_existent.md', empty_project) + expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError) end end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index f5b4882815f..f18823b61ef 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -20,6 +20,34 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true end + it 'returns true for a non-alphanumeric hostname' do + stub_resolv + + aggregate_failures do + expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami/a') + + # The leading character here is a Unicode "soft hyphen" + expect(described_class).to be_blocked_url('ssh://oProxyCommand=whoami/a') + + # Unicode alphanumerics are allowed + expect(described_class).not_to be_blocked_url('ssh://ğitlab.com/a') + end + end + + it 'returns true for a non-alphanumeric username' do + stub_resolv + + aggregate_failures do + expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a') + + # The leading character here is a Unicode "soft hyphen" + expect(described_class).to be_blocked_url('ssh://oProxyCommand=whoami@example.com/a') + + # Unicode alphanumerics are allowed + expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a') + end + end + it 'returns true for invalid URL' do expect(described_class.blocked_url?('http://:8080')).to be true end @@ -28,4 +56,10 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false end end + + # Resolv does not support resolving UTF-8 domain names + # See https://bugs.ruby-lang.org/issues/4270 + def stub_resolv + allow(Resolv).to receive(:getaddresses).and_return([]) + end end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 111c873f79c..92787bb262e 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -1,7 +1,21 @@ require 'spec_helper' describe Gitlab::Utils do - delegate :to_boolean, :boolean_to_yes_no, to: :described_class + delegate :to_boolean, :boolean_to_yes_no, :slugify, to: :described_class + + describe '.slugify' do + { + 'TEST' => 'test', + 'project_with_underscores' => 'project-with-underscores', + 'namespace/project' => 'namespace-project', + 'a' * 70 => 'a' * 63, + 'test_trailing_' => 'test-trailing' + }.each do |original, expected| + it "slugifies #{original} to #{expected}" do + expect(slugify(original)).to eq(expected) + end + end + end describe '.to_boolean' do it 'accepts booleans' do diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index e7e1a92ae54..c8a1e433d59 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -50,8 +50,8 @@ describe 'Gitlab::VersionInfo' do context 'unknown' do it { expect(@unknown).not_to be @v0_0_1 } it { expect(@unknown).not_to be Gitlab::VersionInfo.new } - it { expect{@unknown > @v0_0_1}.to raise_error(ArgumentError) } - it { expect{@unknown < @v0_0_1}.to raise_error(ArgumentError) } + it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) } + it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) } end context 'parse' do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 654397ccffb..b66afafa174 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -202,7 +202,6 @@ describe Gitlab::Workhorse do context 'when Gitaly is enabled' do let(:gitaly_params) do { - GitalyAddress: Gitlab::GitalyClient.address('default'), GitalyServer: { address: Gitlab::GitalyClient.address('default'), token: Gitlab::GitalyClient.token('default') @@ -217,7 +216,9 @@ describe Gitlab::Workhorse do it 'includes a Repository param' do repo_param = { Repository: { storage_name: 'default', - relative_path: project.full_path + '.git' + relative_path: project.full_path + '.git', + git_object_directory: '', + git_alternate_object_directories: [] } } expect(subject).to include(repo_param) diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index e7022bd06f8..d6edc964844 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -27,7 +27,7 @@ describe JSONWebToken::RSAToken do subject { JWT.decode(rsa_encoded, rsa_key) } - it { expect{subject}.not_to raise_error } + it { expect {subject}.not_to raise_error } it { expect(subject.first).to include('key' => 'value') } it do expect(subject.second).to eq( @@ -41,7 +41,7 @@ describe JSONWebToken::RSAToken do let(:new_key) { OpenSSL::PKey::RSA.generate(512) } subject { JWT.decode(rsa_encoded, new_key) } - it { expect{subject}.to raise_error(JWT::DecodeError) } + it { expect {subject}.to raise_error(JWT::DecodeError) } end end end diff --git a/spec/lib/rspec_flaky/example_spec.rb b/spec/lib/rspec_flaky/example_spec.rb new file mode 100644 index 00000000000..5b4fd5ddf3e --- /dev/null +++ b/spec/lib/rspec_flaky/example_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe RspecFlaky::Example do + let(:example_attrs) do + { + id: 'spec/foo/bar_spec.rb:2', + metadata: { + file_path: 'spec/foo/bar_spec.rb', + line_number: 2, + full_description: 'hello world' + }, + execution_result: double(status: 'passed', exception: 'BOOM!'), + attempts: 1 + } + end + let(:rspec_example) { double(example_attrs) } + + describe '#initialize' do + shared_examples 'a valid Example instance' do + it 'returns valid attributes' do + example = described_class.new(args) + + expect(example.example_id).to eq(example_attrs[:id]) + end + end + + context 'when given an Rspec::Core::Example that responds to #example' do + let(:args) { double(example: rspec_example) } + + it_behaves_like 'a valid Example instance' + end + + context 'when given an Rspec::Core::Example that does not respond to #example' do + let(:args) { rspec_example } + + it_behaves_like 'a valid Example instance' + end + end + + subject { described_class.new(rspec_example) } + + describe '#uid' do + it 'returns a hash of the full description' do + expect(subject.uid).to eq(Digest::MD5.hexdigest("#{subject.description}-#{subject.file}")) + end + end + + describe '#example_id' do + it 'returns the ID of the RSpec::Core::Example' do + expect(subject.example_id).to eq(rspec_example.id) + end + end + + describe '#attempts' do + it 'returns the attempts of the RSpec::Core::Example' do + expect(subject.attempts).to eq(rspec_example.attempts) + end + end + + describe '#file' do + it 'returns the metadata[:file_path] of the RSpec::Core::Example' do + expect(subject.file).to eq(rspec_example.metadata[:file_path]) + end + end + + describe '#line' do + it 'returns the metadata[:line_number] of the RSpec::Core::Example' do + expect(subject.line).to eq(rspec_example.metadata[:line_number]) + end + end + + describe '#description' do + it 'returns the metadata[:full_description] of the RSpec::Core::Example' do + expect(subject.description).to eq(rspec_example.metadata[:full_description]) + end + end + + describe '#status' do + it 'returns the execution_result.status of the RSpec::Core::Example' do + expect(subject.status).to eq(rspec_example.execution_result.status) + end + end + + describe '#exception' do + it 'returns the execution_result.exception of the RSpec::Core::Example' do + expect(subject.exception).to eq(rspec_example.execution_result.exception) + end + end +end diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb new file mode 100644 index 00000000000..cbfc1e538ab --- /dev/null +++ b/spec/lib/rspec_flaky/flaky_example_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe RspecFlaky::FlakyExample do + let(:flaky_example_attrs) do + { + example_id: 'spec/foo/bar_spec.rb:2', + file: 'spec/foo/bar_spec.rb', + line: 2, + description: 'hello world', + first_flaky_at: 1234, + last_flaky_at: 2345, + last_attempts_count: 2, + flaky_reports: 1 + } + end + let(:example_attrs) do + { + uid: 'abc123', + example_id: flaky_example_attrs[:example_id], + file: flaky_example_attrs[:file], + line: flaky_example_attrs[:line], + description: flaky_example_attrs[:description], + status: 'passed', + exception: 'BOOM!', + attempts: flaky_example_attrs[:last_attempts_count] + } + end + let(:example) { double(example_attrs) } + + describe '#initialize' do + shared_examples 'a valid FlakyExample instance' do + it 'returns valid attributes' do + flaky_example = described_class.new(args) + + expect(flaky_example.uid).to eq(flaky_example_attrs[:uid]) + expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id]) + end + end + + context 'when given an Rspec::Example' do + let(:args) { example } + + it_behaves_like 'a valid FlakyExample instance' + end + + context 'when given a hash' do + let(:args) { flaky_example_attrs } + + it_behaves_like 'a valid FlakyExample instance' + end + end + + describe '#to_h' do + before do + # Stub these env variables otherwise specs don't behave the same on the CI + stub_env('CI_PROJECT_URL', nil) + stub_env('CI_JOB_ID', nil) + end + + shared_examples 'a valid FlakyExample hash' do + let(:additional_attrs) { {} } + + it 'returns a valid hash' do + flaky_example = described_class.new(args) + final_hash = flaky_example_attrs + .merge(last_flaky_at: instance_of(Time), last_flaky_job: nil) + .merge(additional_attrs) + + expect(flaky_example.to_h).to match(hash_including(final_hash)) + end + end + + context 'when given an Rspec::Example' do + let(:args) { example } + + context 'when run locally' do + it_behaves_like 'a valid FlakyExample hash' do + let(:additional_attrs) do + { first_flaky_at: instance_of(Time) } + end + end + end + + context 'when run on the CI' do + before do + stub_env('CI_PROJECT_URL', 'https://gitlab.com/gitlab-org/gitlab-ce') + stub_env('CI_JOB_ID', 42) + end + + it_behaves_like 'a valid FlakyExample hash' do + let(:additional_attrs) do + { first_flaky_at: instance_of(Time), last_flaky_job: "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42" } + end + end + end + end + + context 'when given a hash' do + let(:args) { flaky_example_attrs } + + it_behaves_like 'a valid FlakyExample hash' + end + end +end diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb new file mode 100644 index 00000000000..0e193bf408b --- /dev/null +++ b/spec/lib/rspec_flaky/listener_spec.rb @@ -0,0 +1,178 @@ +require 'spec_helper' + +describe RspecFlaky::Listener do + let(:flaky_example_report) do + { + 'abc123' => { + example_id: 'spec/foo/bar_spec.rb:2', + file: 'spec/foo/bar_spec.rb', + line: 2, + description: 'hello world', + first_flaky_at: 1234, + last_flaky_at: instance_of(Time), + last_attempts_count: 2, + flaky_reports: 1, + last_flaky_job: nil + } + } + end + let(:example_attrs) do + { + id: 'spec/foo/baz_spec.rb:3', + metadata: { + file_path: 'spec/foo/baz_spec.rb', + line_number: 3, + full_description: 'hello GitLab' + }, + execution_result: double(status: 'passed', exception: nil) + } + end + + before do + # Stub these env variables otherwise specs don't behave the same on the CI + stub_env('CI_PROJECT_URL', nil) + stub_env('CI_JOB_ID', nil) + end + + describe '#initialize' do + shared_examples 'a valid Listener instance' do + let(:expected_all_flaky_examples) { {} } + + it 'returns a valid Listener instance' do + listener = described_class.new + + expect(listener.to_report(listener.all_flaky_examples)) + .to match(hash_including(expected_all_flaky_examples)) + expect(listener.new_flaky_examples).to eq({}) + end + end + + context 'when no report file exists' do + it_behaves_like 'a valid Listener instance' + end + + context 'when a report file exists and set by ALL_FLAKY_RSPEC_REPORT_PATH' do + let(:report_file) do + Tempfile.new(%w[rspec_flaky_report .json]).tap do |f| + f.write(JSON.pretty_generate(flaky_example_report)) + f.rewind + end + end + + before do + stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path) + end + + after do + report_file.close + report_file.unlink + end + + it_behaves_like 'a valid Listener instance' do + let(:expected_all_flaky_examples) { flaky_example_report } + end + end + end + + describe '#example_passed' do + let(:rspec_example) { double(example_attrs) } + let(:notification) { double(example: rspec_example) } + + shared_examples 'a non-flaky example' do + it 'does not change the flaky examples hash' do + expect { subject.example_passed(notification) } + .not_to change { subject.all_flaky_examples } + end + end + + describe 'when the RSpec example does not respond to attempts' do + it_behaves_like 'a non-flaky example' + end + + describe 'when the RSpec example has 1 attempt' do + let(:rspec_example) { double(example_attrs.merge(attempts: 1)) } + + it_behaves_like 'a non-flaky example' + end + + describe 'when the RSpec example has 2 attempts' do + let(:rspec_example) { double(example_attrs.merge(attempts: 2)) } + let(:expected_new_flaky_example) do + { + example_id: 'spec/foo/baz_spec.rb:3', + file: 'spec/foo/baz_spec.rb', + line: 3, + description: 'hello GitLab', + first_flaky_at: instance_of(Time), + last_flaky_at: instance_of(Time), + last_attempts_count: 2, + flaky_reports: 1, + last_flaky_job: nil + } + end + + it 'does not change the flaky examples hash' do + expect { subject.example_passed(notification) } + .to change { subject.all_flaky_examples } + + new_example = RspecFlaky::Example.new(rspec_example) + + expect(subject.all_flaky_examples[new_example.uid].to_h) + .to match(hash_including(expected_new_flaky_example)) + end + end + end + + describe '#dump_summary' do + let(:rspec_example) { double(example_attrs) } + let(:notification) { double(example: rspec_example) } + + context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do + let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') } + + before do + stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path) + FileUtils.rm(report_file_path) if File.exist?(report_file_path) + end + + after do + FileUtils.rm(report_file_path) if File.exist?(report_file_path) + end + + context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do + before do + stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false') + end + + it 'does not write the report file' do + subject.example_passed(notification) + + subject.dump_summary(nil) + + expect(File.exist?(report_file_path)).to be(false) + end + end + + context 'when FLAKY_RSPEC_GENERATE_REPORT == "true"' do + before do + stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true') + end + + it 'writes the report file' do + subject.example_passed(notification) + + subject.dump_summary(nil) + + expect(File.exist?(report_file_path)).to be(true) + end + end + end + end + + describe '#to_report' do + it 'transforms the internal hash to a JSON-ready hash' do + expect(subject.to_report('abc123' => RspecFlaky::FlakyExample.new(flaky_example_report['abc123']))) + .to match(hash_including(flaky_example_report)) + end + end +end diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb index 025ea2673b4..4de5da984ba 100644 --- a/spec/lib/system_check/simple_executor_spec.rb +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -240,7 +240,7 @@ describe SystemCheck::SimpleExecutor do context 'when there is an exception' do it 'rescues the exception' do - expect{ subject.run_check(BugousCheck) }.not_to raise_exception + expect { subject.run_check(BugousCheck) }.not_to raise_exception end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e36d7a1800c..1fa59ebd22b 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -545,7 +545,7 @@ describe Notify do let(:note_author) { create(:user, name: 'author_name') } let(:note) { create(:note, project: project, author: note_author) } - before :each do + before do allow(Note).to receive(:find).with(note.id).and_return(note) end @@ -661,7 +661,7 @@ describe Notify do let(:project) { create(:project, :repository) } let(:note_author) { create(:user, name: 'author_name') } - before :each do + before do allow(Note).to receive(:find).with(note.id).and_return(note) end @@ -779,7 +779,7 @@ describe Notify do context 'items that are noteable, the email for a diff discussion note' do let(:note_author) { create(:user, name: 'author_name') } - before :each do + before do allow(Note).to receive(:find).with(note.id).and_return(note) end diff --git a/spec/migrations/README.md b/spec/migrations/README.md index 05d4f35db72..45cf25b96de 100644 --- a/spec/migrations/README.md +++ b/spec/migrations/README.md @@ -28,6 +28,14 @@ The `after` hook will migrate the database **up** and reinstitutes the latest schema version, so that the process does not affect subsequent specs and ensures proper isolation. +## Testing a class that is not an ActiveRecord::Migration + +In order to test a class that is not a migration itself, you will need to +manually provide a required schema version. Please add a `schema` tag to a +context that you want to switch the database schema within. + +Example: `describe SomeClass, :migration, schema: 20170608152748`. + ## Available helpers Use `table` helper to create a temporary `ActiveRecord::Base` derived model @@ -80,8 +88,6 @@ end ## Best practices -1. Use only one test example per migration unless there is a good reason to -use more. 1. Note that this type of tests do not run within the transaction, we use a truncation database cleanup strategy. Do not depend on transaction being present. diff --git a/spec/migrations/clean_upload_symlinks_spec.rb b/spec/migrations/clean_upload_symlinks_spec.rb index cecb3ddac53..26653b9c008 100644 --- a/spec/migrations/clean_upload_symlinks_spec.rb +++ b/spec/migrations/clean_upload_symlinks_spec.rb @@ -5,7 +5,7 @@ describe CleanUploadSymlinks do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_uploads_test") } let(:uploads_dir) { File.join(test_dir, "public", "uploads") } - let(:new_uploads_dir) { File.join(uploads_dir, "system") } + let(:new_uploads_dir) { File.join(uploads_dir, "-", "system") } let(:original_path) { File.join(new_uploads_dir, 'user') } let(:symlink_path) { File.join(uploads_dir, 'user') } diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb index cfe1ca481b2..81366d15b34 100644 --- a/spec/migrations/migrate_old_artifacts_spec.rb +++ b/spec/migrations/migrate_old_artifacts_spec.rb @@ -10,7 +10,7 @@ describe MigrateOldArtifacts do before do allow(Gitlab.config.artifacts).to receive(:path).and_return(directory) end - + after do FileUtils.remove_entry_secure(directory) end @@ -95,7 +95,7 @@ describe MigrateOldArtifacts do FileUtils.copy( Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), File.join(legacy_path(build), "ci_build_artifacts.zip")) - + FileUtils.copy( Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), File.join(legacy_path(build), "ci_build_artifacts_metadata.gz")) diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index cf2d5827306..e5793a3c0ee 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -6,7 +6,7 @@ require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_ describe MigrateProcessCommitWorkerJobs do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:commit) { project.commit.raw.raw_commit } + let(:commit) { project.commit.raw.rugged_commit } describe 'Project' do describe 'find_including_path' do diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb index 8505c7bf3e3..1a319eccc0d 100644 --- a/spec/migrations/move_personal_snippets_files_spec.rb +++ b/spec/migrations/move_personal_snippets_files_spec.rb @@ -5,7 +5,7 @@ describe MovePersonalSnippetsFiles do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_snippet_files_test") } let(:uploads_dir) { File.join(test_dir, 'uploads') } - let(:new_uploads_dir) { File.join(uploads_dir, 'system') } + let(:new_uploads_dir) { File.join(uploads_dir, '-', 'system') } before do allow(CarrierWave).to receive(:root).and_return(test_dir) @@ -42,7 +42,7 @@ describe MovePersonalSnippetsFiles do describe 'updating the markdown' do it 'includes the new path when the file exists' do secret = "secret#{snippet.id}" - file_location = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" + file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" migration.up @@ -60,7 +60,7 @@ describe MovePersonalSnippetsFiles do it 'updates the note markdown' do secret = "secret#{snippet.id}" - file_location = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" + file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" markdown = markdown_linking_file('picture.jpg', snippet) note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") @@ -108,7 +108,7 @@ describe MovePersonalSnippetsFiles do it 'keeps the markdown as is when the file is missing' do secret = "secret#{snippet_with_missing_file.id}" - file_location = "/uploads/system/personal_snippet/#{snippet_with_missing_file.id}/#{secret}/picture.jpg" + file_location = "/uploads/-/system/personal_snippet/#{snippet_with_missing_file.id}/#{secret}/picture.jpg" migration.down @@ -167,7 +167,7 @@ describe MovePersonalSnippetsFiles do def markdown_linking_file(filename, snippet, in_new_path: false) markdown = "![#{filename.split('.')[0]}]" markdown += '(/uploads' - markdown += '/system' if in_new_path + markdown += '/-/system' if in_new_path markdown += "/#{model_file_path(filename, snippet)})" markdown end diff --git a/spec/migrations/move_system_upload_folder_spec.rb b/spec/migrations/move_system_upload_folder_spec.rb index b622b4e9536..d3180477db3 100644 --- a/spec/migrations/move_system_upload_folder_spec.rb +++ b/spec/migrations/move_system_upload_folder_spec.rb @@ -33,6 +33,15 @@ describe MoveSystemUploadFolder do expect(File.symlink?(File.join(test_base, 'system'))).to be_truthy expect(File.exist?(File.join(test_base, 'system', 'file'))).to be_truthy end + + it 'does not move if the target directory already exists' do + FileUtils.mkdir_p(File.join(test_base, '-', 'system')) + + expect(FileUtils).not_to receive(:mv) + expect(migration).to receive(:say).with(/already exists. No need to redo the move/) + + migration.up + end end describe '#down' do @@ -58,5 +67,14 @@ describe MoveSystemUploadFolder do expect(File.directory?(File.join(test_base, 'system'))).to be_truthy expect(File.symlink?(File.join(test_base, 'system'))).to be_falsey end + + it 'does not move if the old directory already exists' do + FileUtils.mkdir_p(File.join(test_base, 'system')) + + expect(FileUtils).not_to receive(:mv) + expect(migration).to receive(:say).with(/already exists and is not a symlink, no need to revert/) + + migration.down + end end end diff --git a/spec/migrations/move_uploads_to_system_dir_spec.rb b/spec/migrations/move_uploads_to_system_dir_spec.rb index 37d66452447..ca11a2004c5 100644 --- a/spec/migrations/move_uploads_to_system_dir_spec.rb +++ b/spec/migrations/move_uploads_to_system_dir_spec.rb @@ -5,7 +5,7 @@ describe MoveUploadsToSystemDir do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "move_uploads_test") } let(:uploads_dir) { File.join(test_dir, "public", "uploads") } - let(:new_uploads_dir) { File.join(uploads_dir, "system") } + let(:new_uploads_dir) { File.join(uploads_dir, "-", "system") } before do FileUtils.remove_dir(test_dir) if File.directory?(test_dir) diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb new file mode 100644 index 00000000000..e393374028f --- /dev/null +++ b/spec/migrations/remove_duplicate_mr_events_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170815060945_remove_duplicate_mr_events.rb') + +describe RemoveDuplicateMrEvents, truncate: true do + let(:migration) { described_class.new } + + describe '#up' do + let(:user) { create(:user) } + let(:merge_requests) { create_list(:merge_request, 2) } + let(:issue) { create(:issue) } + let!(:events) do + [ + create(:event, :created, author: user, target: merge_requests.first), + create(:event, :created, author: user, target: merge_requests.first), + create(:event, :updated, author: user, target: merge_requests.first), + create(:event, :created, author: user, target: merge_requests.second), + create(:event, :created, author: user, target: issue), + create(:event, :created, author: user, target: issue) + ] + end + + it 'removes duplicated merge request create records' do + expect { migration.up }.to change { Event.count }.from(6).to(5) + end + end +end diff --git a/spec/migrations/rename_system_namespaces_spec.rb b/spec/migrations/rename_system_namespaces_spec.rb deleted file mode 100644 index 747694cbe33..00000000000 --- a/spec/migrations/rename_system_namespaces_spec.rb +++ /dev/null @@ -1,254 +0,0 @@ -require "spec_helper" -require Rails.root.join("db", "migrate", "20170316163800_rename_system_namespaces.rb") - -describe RenameSystemNamespaces, truncate: true do - let(:migration) { described_class.new } - let(:test_dir) { File.join(Rails.root, "tmp", "tests", "rename_namespaces_test") } - let(:uploads_dir) { File.join(test_dir, "public", "uploads") } - let(:system_namespace) do - namespace = build(:namespace, path: "system") - namespace.save(validate: false) - namespace - end - - def save_invalid_routable(routable) - routable.__send__(:prepare_route) - routable.save(validate: false) - end - - before do - FileUtils.remove_dir(test_dir) if File.directory?(test_dir) - FileUtils.mkdir_p(uploads_dir) - FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path) - allow(migration).to receive(:say) - allow(migration).to receive(:uploads_dir).and_return(uploads_dir) - end - - describe "#system_namespace" do - it "only root namespaces called with path `system`" do - system_namespace - system_namespace_with_parent = build(:namespace, path: 'system', parent: create(:namespace)) - system_namespace_with_parent.save(validate: false) - - expect(migration.system_namespace.id).to eq(system_namespace.id) - end - end - - describe "#up" do - before do - system_namespace - end - - it "doesn't break if there are no namespaces called system" do - Namespace.delete_all - - migration.up - end - - it "renames namespaces called system" do - migration.up - - expect(system_namespace.reload.path).to eq("system0") - end - - it "renames the route to the namespace" do - migration.up - - expect(system_namespace.reload.full_path).to eq("system0") - end - - it "renames the route for projects of the namespace" do - project = build(:project, :repository, path: "project-path", namespace: system_namespace) - save_invalid_routable(project) - - migration.up - - expect(project.route.reload.path).to eq("system0/project-path") - end - - it "doesn't touch routes of namespaces that look like system" do - namespace = create(:group, path: 'systemlookalike') - project = create(:project, :repository, namespace: namespace, path: 'the-project') - - migration.up - - expect(project.route.reload.path).to eq('systemlookalike/the-project') - expect(namespace.route.reload.path).to eq('systemlookalike') - end - - it "moves the the repository for a project in the namespace" do - project = build(:project, :repository, namespace: system_namespace, path: "system-project") - save_invalid_routable(project) - TestEnv.copy_repo(project, - bare_repo: TestEnv.factory_repo_path_bare, - refs: TestEnv::BRANCH_SHA) - expected_repo = File.join(TestEnv.repos_path, "system0", "system-project.git") - - migration.up - - expect(File.directory?(expected_repo)).to be(true) - end - - it "moves the uploads for the namespace" do - allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") - expect(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") - - migration.up - end - - it "moves the pages for the namespace" do - allow(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") - expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") - - migration.up - end - - describe "clears the markdown cache for projects in the system namespace" do - let!(:project) do - project = build(:project, :repository, namespace: system_namespace) - save_invalid_routable(project) - project - end - - it 'removes description_html from projects' do - migration.up - - expect(project.reload.description_html).to be_nil - end - - it 'removes issue descriptions' do - issue = create(:issue, project: project, description_html: 'Issue description') - - migration.up - - expect(issue.reload.description_html).to be_nil - end - - it 'removes merge request descriptions' do - merge_request = create(:merge_request, - source_project: project, - target_project: project, - description_html: 'MergeRequest description') - - migration.up - - expect(merge_request.reload.description_html).to be_nil - end - - it 'removes note html' do - note = create(:note, - project: project, - noteable: create(:issue, project: project), - note_html: 'note description') - - migration.up - - expect(note.reload.note_html).to be_nil - end - - it 'removes milestone description' do - milestone = create(:milestone, - project: project, - description_html: 'milestone description') - - migration.up - - expect(milestone.reload.description_html).to be_nil - end - end - - context "system namespace -> subgroup -> system0 project" do - it "updates the route of the project correctly" do - subgroup = build(:group, path: "subgroup", parent: system_namespace) - save_invalid_routable(subgroup) - project = build(:project, :repository, path: "system0", namespace: subgroup) - save_invalid_routable(project) - - migration.up - - expect(project.route.reload.path).to eq("system0/subgroup/system0") - end - end - end - - describe "#move_repositories" do - let(:namespace) { create(:group, name: "hello-group") } - it "moves a project for a namespace" do - create(:project, :repository, namespace: namespace, path: "hello-project") - expected_path = File.join(TestEnv.repos_path, "bye-group", "hello-project.git") - - migration.move_repositories(namespace, "hello-group", "bye-group") - - expect(File.directory?(expected_path)).to be(true) - end - - it "moves a namespace in a subdirectory correctly" do - child_namespace = create(:group, name: "sub-group", parent: namespace) - create(:project, :repository, namespace: child_namespace, path: "hello-project") - - expected_path = File.join(TestEnv.repos_path, "hello-group", "renamed-sub-group", "hello-project.git") - - migration.move_repositories(child_namespace, "hello-group/sub-group", "hello-group/renamed-sub-group") - - expect(File.directory?(expected_path)).to be(true) - end - - it "moves a parent namespace with subdirectories" do - child_namespace = create(:group, name: "sub-group", parent: namespace) - create(:project, :repository, namespace: child_namespace, path: "hello-project") - expected_path = File.join(TestEnv.repos_path, "renamed-group", "sub-group", "hello-project.git") - - migration.move_repositories(child_namespace, "hello-group", "renamed-group") - - expect(File.directory?(expected_path)).to be(true) - end - end - - describe "#move_namespace_folders" do - it "moves a namespace with files" do - source = File.join(uploads_dir, "parent-group", "sub-group") - FileUtils.mkdir_p(source) - destination = File.join(uploads_dir, "parent-group", "moved-group") - FileUtils.touch(File.join(source, "test.txt")) - expected_file = File.join(destination, "test.txt") - - migration.move_namespace_folders(uploads_dir, File.join("parent-group", "sub-group"), File.join("parent-group", "moved-group")) - - expect(File.exist?(expected_file)).to be(true) - end - - it "moves a parent namespace uploads" do - source = File.join(uploads_dir, "parent-group", "sub-group") - FileUtils.mkdir_p(source) - destination = File.join(uploads_dir, "moved-parent", "sub-group") - FileUtils.touch(File.join(source, "test.txt")) - expected_file = File.join(destination, "test.txt") - - migration.move_namespace_folders(uploads_dir, "parent-group", "moved-parent") - - expect(File.exist?(expected_file)).to be(true) - end - end - - describe "#child_ids_for_parent" do - it "collects child ids for all levels" do - parent = create(:group) - first_child = create(:group, parent: parent) - second_child = create(:group, parent: parent) - third_child = create(:group, parent: second_child) - all_ids = [parent.id, first_child.id, second_child.id, third_child.id] - - collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id]) - - expect(collected_ids).to contain_exactly(*all_ids) - end - end - - describe "#remove_last_ocurrence" do - it "removes only the last occurance of a string" do - input = "this/is/system/namespace/with/system" - - expect(migration.remove_last_occurrence(input, "system")).to eq("this/is/system/namespace/with/") - end - end -end diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb index 11412005b72..0a45c5ea32d 100644 --- a/spec/migrations/update_upload_paths_to_system_spec.rb +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -11,7 +11,7 @@ describe UpdateUploadPathsToSystem do describe "#uploads_to_switch_to_new_path" do it "contains only uploads with the old path for the correct models" do _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg") + _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") @@ -23,7 +23,7 @@ describe UpdateUploadPathsToSystem do describe "#uploads_to_switch_to_old_path" do it "contains only uploads with the new path for the correct models" do _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - upload_with_system_path = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg") + upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") _old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") @@ -37,13 +37,13 @@ describe UpdateUploadPathsToSystem do migration.up - expect(old_upload.reload.path).to eq("uploads/system/project/avatar.jpg") + expect(old_upload.reload.path).to eq("uploads/-/system/project/avatar.jpg") end end describe "#down", truncate: true do it "updates the new system patsh to the old paths" do - new_upload = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg") + new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") migration.down diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 7cd3a84d592..b5d5d58697b 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -9,4 +9,39 @@ RSpec.describe Appearance do it { is_expected.to validate_presence_of(:description) } it { is_expected.to have_many(:uploads).dependent(:destroy) } + + describe '.current', :use_clean_rails_memory_store_caching do + let!(:appearance) { create(:appearance) } + + it 'returns the current appearance row' do + expect(described_class.current).to eq(appearance) + end + + it 'caches the result' do + expect(described_class).to receive(:first).once + + 2.times { described_class.current } + end + end + + describe '#flush_redis_cache' do + it 'flushes the cache in Redis' do + appearance = create(:appearance) + + expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY) + + appearance.flush_redis_cache + end + end + + describe '#single_appearance_row' do + it 'adds an error when more than 1 row exists' do + create(:appearance) + + new_row = build(:appearance) + new_row.save + + expect(new_row.valid?).to eq(false) + end + end end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index a8ca1d110e4..3369aef1d3e 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -20,7 +20,7 @@ describe BroadcastMessage do it { is_expected.not_to allow_value('000').for(:font) } end - describe '.current' do + describe '.current', :use_clean_rails_memory_store_caching do it 'returns message if time match' do message = create(:broadcast_message) @@ -45,6 +45,14 @@ describe BroadcastMessage do expect(described_class.current).to be_empty end + + it 'caches the output of the query' do + create(:broadcast_message) + + expect(described_class).to receive(:where).and_call_original.once + + 2.times { described_class.current } + end end describe '#active?' do @@ -102,4 +110,14 @@ describe BroadcastMessage do end end end + + describe '#flush_redis_cache' do + it 'flushes the Redis cache' do + message = create(:broadcast_message) + + expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY) + + message.flush_redis_cache + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 86afa856ea7..767f0ad9e65 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1220,7 +1220,7 @@ describe Ci::Build do { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path.parameterize, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 08693b5da33..c18c635d811 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -33,7 +33,6 @@ describe Commit do describe '#to_reference' do let(:project) { create(:project, :repository, path: 'sample-project') } - let(:commit) { project.commit } it 'returns a String reference to the object' do expect(commit.to_reference).to eq commit.id @@ -47,7 +46,6 @@ describe Commit do describe '#reference_link_text' do let(:project) { create(:project, :repository, path: 'sample-project') } - let(:commit) { project.commit } it 'returns a String reference to the object' do expect(commit.reference_link_text).to eq commit.short_id @@ -191,7 +189,7 @@ eos it { expect(data).to be_a(Hash) } it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') } - it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') } + it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') } it { expect(data[:added]).to eq(["bar/branch-test.txt"]) } it { expect(data[:modified]).to eq([]) } it { expect(data[:removed]).to eq([]) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 6fb4794ea5f..8c4a366ef8f 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -425,7 +425,7 @@ describe CommitStatus do end it "raise exception when trying to update" do - expect{ commit_status.save }.to raise_error(ActiveRecord::StaleObjectError) + expect { commit_status.save }.to raise_error(ActiveRecord::StaleObjectError) end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 0137f71be8f..dfbe1a7c192 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -300,7 +300,7 @@ describe Issuable do let(:bug) { create(:label, project: project, title: 'bug') } let(:issue) { create(:issue, project: project) } - before(:each) do + before do issue.labels << bug end @@ -402,7 +402,7 @@ describe Issuable do let(:issue2) { create(:issue, title: "Bugfix2", project: project) } let(:issue3) { create(:issue, title: "Feature1", project: project) } - before(:each) do + before do issue1.labels << bug issue1.labels << feature issue2.labels << bug diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 5f9b7e0a367..a5d505af001 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -31,7 +31,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do let(:now) { Time.now.utc } - around(:each) do |example| + around do |example| Timecop.freeze(now) { example.run } end diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb new file mode 100644 index 00000000000..e0a87c18cc7 --- /dev/null +++ b/spec/models/event_collection_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe EventCollection do + describe '#to_a' do + let(:project) { create(:project_empty_repo) } + let(:projects) { Project.where(id: project.id) } + let(:user) { create(:user) } + + before do + 20.times do + event = create(:push_event, project: project, author: user) + + create(:push_event_payload, event: event) + end + + create(:closed_issue_event, project: project, author: user) + end + + it 'returns an Array of events' do + events = described_class.new(projects).to_a + + expect(events).to be_an_instance_of(Array) + end + + it 'applies a limit to the number of events' do + events = described_class.new(projects).to_a + + expect(events.length).to eq(20) + end + + it 'can paginate through events' do + events = described_class.new(projects, offset: 20).to_a + + expect(events.length).to eq(1) + end + + it 'returns an empty Array when crossing the maximum page number' do + events = described_class.new(projects, limit: 1, offset: 15).to_a + + expect(events).to be_empty + end + + it 'allows filtering of events using an EventFilter' do + filter = EventFilter.new(EventFilter.issue) + events = described_class.new(projects, filter: filter).to_a + + expect(events.length).to eq(1) + expect(events[0].action).to eq(Event::CLOSED) + end + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index d86bf1a90a9..ff3224dd298 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -304,27 +304,15 @@ describe Event do end end - def create_push_event(project, user, attrs = {}) - data = { - before: Gitlab::Git::BLANK_SHA, - after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", - ref: "refs/heads/master", - user_id: user.id, - user_name: user.name, - repository: { - name: project.name, - url: "localhost/rubinius", - description: "", - homepage: "localhost/rubinius", - private: true - } - } - - described_class.create({ - project: project, - action: described_class::PUSHED, - data: data, - author_id: user.id - }.merge!(attrs)) + def create_push_event(project, user) + event = create(:push_event, project: project, author: user) + + create(:push_event_payload, + event: event, + commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + commit_count: 0, + ref: 'master') + + event end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index c055863d298..9203f6562f2 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -57,18 +57,14 @@ describe Issue do end describe '#closed_at' do - after do - Timecop.return - end - - let!(:now) { Timecop.freeze(Time.now) } - it 'sets closed_at to Time.now when issue is closed' do issue = create(:issue, state: 'opened') + expect(issue.closed_at).to be_nil + issue.close - expect(issue.closed_at).to eq(now) + expect(issue.closed_at).to be_present end end @@ -350,7 +346,7 @@ describe Issue do subject { create(:issue, project: create(:project, :repository)) } let(:backref_text) { "issue #{subject.to_reference}" } - let(:set_mentionable_text) { ->(txt){ subject.description = txt } } + let(:set_mentionable_text) { ->(txt) { subject.description = txt } } end it_behaves_like 'a Taskable' do diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index f1d1f37c78a..fa3e80ba062 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -149,7 +149,7 @@ describe ProjectMember do describe 'notifications' do describe '#after_accept_request' do it 'calls NotificationService.new_project_member' do - member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now) + member = create(:project_member, user: create(:user), requested_at: Time.now) expect_any_instance_of(NotificationService).to receive(:new_project_member) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a1a3e70a7d2..026bdbd26d1 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -681,7 +681,7 @@ describe MergeRequest do end it 'does not crash' do - expect{ subject.diverged_commits_count }.not_to raise_error + expect { subject.diverged_commits_count }.not_to raise_error end it 'returns 0' do @@ -714,7 +714,7 @@ describe MergeRequest do end describe 'caching' do - before(:example) do + before do allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) end @@ -753,7 +753,7 @@ describe MergeRequest do subject { create(:merge_request, :simple) } let(:backref_text) { "merge request #{subject.to_reference}" } - let(:set_mentionable_text) { ->(txt){ subject.description = txt } } + let(:set_mentionable_text) { ->(txt) { subject.description = txt } } end it_behaves_like 'a Taskable' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 1a00c50690c..69286eff984 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -315,6 +315,20 @@ describe Namespace do end end + describe '#self_and_ancestors', :nested_groups do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct ancestors' do + expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) + expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group) + expect(group.self_and_ancestors).to contain_exactly(group) + end + end + describe '#descendants', :nested_groups do let!(:group) { create(:group, path: 'git_lab') } let!(:nested_group) { create(:group, parent: group) } @@ -331,6 +345,22 @@ describe Namespace do end end + describe '#self_and_descendants', :nested_groups do + let!(:group) { create(:group, path: 'git_lab') } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let!(:another_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + + it 'returns the correct descendants' do + expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) + expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group) + expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group) + expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + end + end + describe '#users_with_descendants', :nested_groups do let(:user_a) { create(:user) } let(:user_b) { create(:user) } diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 5b0f24ce306..d8972aff407 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -43,7 +43,7 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } - before(:each) do + before do allow(drone).to receive_messages( project_id: project.id, project: project, diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 7614bb897e8..23db29cb541 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -36,7 +36,7 @@ describe HipchatService do Gitlab::DataBuilder::Push.build_sample(project, user) end - before(:each) do + before do allow(hipchat).to receive_messages( project_id: project.id, project: project, diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8f951605954..5e60511f3a8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -485,7 +485,7 @@ describe Project do describe 'last_activity' do it 'alias last_activity to last_event' do - last_event = create(:event, project: project) + last_event = create(:event, :closed, project: project) expect(project.last_activity).to eq(last_event) end @@ -493,7 +493,7 @@ describe Project do describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do - new_event = create(:event, project: project, created_at: Time.now) + new_event = create(:event, :closed, project: project, created_at: Time.now) project.reload expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i) @@ -651,7 +651,7 @@ describe Project do let(:ext_project) { create(:redmine_project) } context 'on existing projects with no value for has_external_issue_tracker' do - before(:each) do + before do project.update_column(:has_external_issue_tracker, nil) ext_project.update_column(:has_external_issue_tracker, nil) end @@ -1301,7 +1301,7 @@ describe Project do subject { project.rename_repo } - it { expect{subject}.to raise_error(StandardError) } + it { expect {subject}.to raise_error(StandardError) } end end @@ -1610,8 +1610,7 @@ describe Project do it 'imports a project' do expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original - project.import_schedule - + expect { project.import_schedule }.to change { project.import_jid } expect(project.reload.import_status).to eq('finished') end end @@ -1624,6 +1623,13 @@ describe Project do allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service } end + it 'resets project import_error' do + error_message = 'Some error' + mirror = create(:project_empty_repo, :import_started, import_error: error_message) + + expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil) + end + it 'performs housekeeping when an import of a fresh project is completed' do project = create(:project_empty_repo, :import_started, import_type: :github) @@ -1730,17 +1736,21 @@ describe Project do end describe '#add_import_job' do + let(:import_jid) { '123' } + context 'forked' do let(:forked_project_link) { create(:forked_project_link, :forked_to_empty_project) } let(:forked_from_project) { forked_project_link.forked_from_project } let(:project) { forked_project_link.forked_to_project } it 'schedules a RepositoryForkWorker job' do - expect(RepositoryForkWorker).to receive(:perform_async) - .with(project.id, forked_from_project.repository_storage_path, - forked_from_project.disk_path, project.namespace.full_path) + expect(RepositoryForkWorker).to receive(:perform_async).with( + project.id, + forked_from_project.repository_storage_path, + forked_from_project.disk_path, + project.namespace.full_path).and_return(import_jid) - project.add_import_job + expect(project.add_import_job).to eq(import_jid) end end @@ -1748,9 +1758,8 @@ describe Project do it 'schedules a RepositoryImportWorker job' do project = create(:project, import_url: generate(:url)) - expect(RepositoryImportWorker).to receive(:perform_async).with(project.id) - - project.add_import_job + expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid) + expect(project.add_import_job).to eq(import_jid) end end end @@ -1832,6 +1841,11 @@ describe Project do describe '#change_head' do let(:project) { create(:project, :repository) } + it 'returns error if branch does not exist' do + expect(project.change_head('unexisted-branch')).to be false + expect(project.errors.size).to eq(1) + end + it 'calls the before_change_head and after_change_head methods' do expect(project.repository).to receive(:before_change_head) expect(project.repository).to receive(:after_change_head) @@ -2305,4 +2319,52 @@ describe Project do end end end + + describe '#remove_pages' do + let(:project) { create(:project) } + let(:namespace) { project.namespace } + let(:pages_path) { project.pages_path } + + around do |example| + FileUtils.mkdir_p(pages_path) + begin + example.run + ensure + FileUtils.rm_rf(pages_path) + end + end + + it 'removes the pages directory' do + expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute) + expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true) + expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything) + + project.remove_pages + end + + it 'is a no-op when there is no namespace' do + project.update_column(:namespace_id, nil) + + expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute) + expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project) + + project.remove_pages + end + + it 'is run when the project is destroyed' do + expect(project).to receive(:remove_pages).and_call_original + + project.destroy + end + end + + describe '#forks_count' do + it 'returns the number of forks' do + project = build(:project) + + allow(project.forks).to receive(:count).and_return(1) + + expect(project.forks_count).to eq(1) + end + end end diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb index 5c5dcd9f5c9..d4433a88a15 100644 --- a/spec/models/protectable_dropdown_spec.rb +++ b/spec/models/protectable_dropdown_spec.rb @@ -4,6 +4,13 @@ describe ProtectableDropdown do let(:project) { create(:project, :repository) } let(:subject) { described_class.new(project, :branches) } + describe 'initialize' do + it 'raises ArgumentError for invalid ref type' do + expect { described_class.new(double, :foo) } + .to raise_error(ArgumentError, "invalid ref type `foo`") + end + end + describe '#protectable_ref_names' do before do project.protected_branches.create(name: 'master') diff --git a/spec/models/push_event_payload_spec.rb b/spec/models/push_event_payload_spec.rb new file mode 100644 index 00000000000..a049ad35584 --- /dev/null +++ b/spec/models/push_event_payload_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe PushEventPayload do + describe 'saving payloads' do + it 'does not allow commit messages longer than 70 characters' do + event = create(:push_event) + payload = build(:push_event_payload, event: event) + + expect(payload).to be_valid + + payload.commit_title = 'a' * 100 + + expect(payload).not_to be_valid + end + end +end diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb new file mode 100644 index 00000000000..532fb024261 --- /dev/null +++ b/spec/models/push_event_spec.rb @@ -0,0 +1,202 @@ +require 'spec_helper' + +describe PushEvent do + let(:payload) { PushEventPayload.new } + + let(:event) do + event = described_class.new + + allow(event).to receive(:push_event_payload).and_return(payload) + + event + end + + describe '.sti_name' do + it 'returns Event::PUSHED' do + expect(described_class.sti_name).to eq(Event::PUSHED) + end + end + + describe '#push?' do + it 'returns true' do + expect(event).to be_push + end + end + + describe '#push_with_commits?' do + it 'returns true when both the first and last commit are present' do + allow(event).to receive(:commit_from).and_return('123') + allow(event).to receive(:commit_to).and_return('456') + + expect(event).to be_push_with_commits + end + + it 'returns false when the first commit is missing' do + allow(event).to receive(:commit_to).and_return('456') + + expect(event).not_to be_push_with_commits + end + + it 'returns false when the last commit is missing' do + allow(event).to receive(:commit_from).and_return('123') + + expect(event).not_to be_push_with_commits + end + end + + describe '#tag?' do + it 'returns true when pushing to a tag' do + allow(payload).to receive(:tag?).and_return(true) + + expect(event).to be_tag + end + + it 'returns false when pushing to a branch' do + allow(payload).to receive(:tag?).and_return(false) + + expect(event).not_to be_tag + end + end + + describe '#branch?' do + it 'returns true when pushing to a branch' do + allow(payload).to receive(:branch?).and_return(true) + + expect(event).to be_branch + end + + it 'returns false when pushing to a tag' do + allow(payload).to receive(:branch?).and_return(false) + + expect(event).not_to be_branch + end + end + + describe '#valid_push?' do + it 'returns true if a ref exists' do + allow(payload).to receive(:ref).and_return('master') + + expect(event).to be_valid_push + end + + it 'returns false when no ref is present' do + expect(event).not_to be_valid_push + end + end + + describe '#new_ref?' do + it 'returns true when pushing a new ref' do + allow(payload).to receive(:created?).and_return(true) + + expect(event).to be_new_ref + end + + it 'returns false when pushing to an existing ref' do + allow(payload).to receive(:created?).and_return(false) + + expect(event).not_to be_new_ref + end + end + + describe '#rm_ref?' do + it 'returns true when removing an existing ref' do + allow(payload).to receive(:removed?).and_return(true) + + expect(event).to be_rm_ref + end + + it 'returns false when pushing to an existing ref' do + allow(payload).to receive(:removed?).and_return(false) + + expect(event).not_to be_rm_ref + end + end + + describe '#commit_from' do + it 'returns the first commit SHA' do + allow(payload).to receive(:commit_from).and_return('123') + + expect(event.commit_from).to eq('123') + end + end + + describe '#commit_to' do + it 'returns the last commit SHA' do + allow(payload).to receive(:commit_to).and_return('123') + + expect(event.commit_to).to eq('123') + end + end + + describe '#ref_name' do + it 'returns the name of the ref' do + allow(payload).to receive(:ref).and_return('master') + + expect(event.ref_name).to eq('master') + end + end + + describe '#ref_type' do + it 'returns the type of the ref' do + allow(payload).to receive(:ref_type).and_return('branch') + + expect(event.ref_type).to eq('branch') + end + end + + describe '#branch_name' do + it 'returns the name of the branch' do + allow(payload).to receive(:ref).and_return('master') + + expect(event.branch_name).to eq('master') + end + end + + describe '#tag_name' do + it 'returns the name of the tag' do + allow(payload).to receive(:ref).and_return('1.2') + + expect(event.tag_name).to eq('1.2') + end + end + + describe '#commit_title' do + it 'returns the commit message' do + allow(payload).to receive(:commit_title).and_return('foo') + + expect(event.commit_title).to eq('foo') + end + end + + describe '#commit_id' do + it 'returns the SHA of the last commit if present' do + allow(event).to receive(:commit_to).and_return('123') + + expect(event.commit_id).to eq('123') + end + + it 'returns the SHA of the first commit if the last commit is not present' do + allow(event).to receive(:commit_to).and_return(nil) + allow(event).to receive(:commit_from).and_return('123') + + expect(event.commit_id).to eq('123') + end + end + + describe '#commits_count' do + it 'returns the number of commits' do + allow(payload).to receive(:commit_count).and_return(1) + + expect(event.commits_count).to eq(1) + end + end + + describe '#validate_push_action' do + it 'adds an error when the action is not PUSHED' do + event.action = Event::CREATED + event.validate_push_action + + expect(event.errors.count).to eq(1) + end + end +end diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb index 80943877095..106ae59af29 100644 --- a/spec/models/redirect_route_spec.rb +++ b/spec/models/redirect_route_spec.rb @@ -20,8 +20,16 @@ describe RedirectRoute do let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') } let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') } - it 'returns correct routes' do - expect(described_class.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5]) + context 'when the redirect route matches with same casing' do + it 'returns correct routes' do + expect(described_class.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5]) + end + end + + context 'when the redirect route matches with different casing' do + it 'returns correct routes' do + expect(described_class.matching_path_and_descendants('GitLABB/test')).to match_array([redirect2, redirect3, redirect4, redirect5]) + end end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index cfa77648338..4926d5d6c49 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -961,6 +961,27 @@ describe Repository, models: true do end end + context 'when temporary ref failed to be created from other project' do + let(:target_project) { create(:project, :empty_repo) } + + before do + expect(target_project.repository).to receive(:run_git) + end + + it 'raises Rugged::ReferenceError' do + raise_reference_error = raise_error(Rugged::ReferenceError) do |err| + expect(err.cause).to be_nil + end + + expect do + GitOperationService.new(user, target_project.repository) + .with_branch('feature', + start_project: project, + &:itself) + end.to raise_reference_error + end + end + context 'when the update adds more than one commit' do let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' } @@ -1224,7 +1245,7 @@ describe Repository, models: true do end describe 'skip_merges option' do - subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } } + subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map { |k| k.id } } it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index bdacc60fb53..fece370c03f 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -145,45 +145,71 @@ describe Route do describe '#delete_conflicting_redirects' do context 'when a redirect route with the same path exists' do - let!(:redirect1) { route.create_redirect(route.path) } + context 'when the redirect route has matching case' do + let!(:redirect1) { route.create_redirect(route.path) } - it 'deletes the redirect' do - route.delete_conflicting_redirects - expect(route.conflicting_redirects).to be_empty + it 'deletes the redirect' do + expect do + route.delete_conflicting_redirects + end.to change { RedirectRoute.count }.by(-1) + end + + context 'when redirect routes with paths descending from the route path exists' do + let!(:redirect2) { route.create_redirect("#{route.path}/foo") } + let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") } + let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") } + let!(:other_redirect) { route.create_redirect("other") } + + it 'deletes all redirects with paths that descend from the route path' do + expect do + route.delete_conflicting_redirects + end.to change { RedirectRoute.count }.by(-4) + end + end end - context 'when redirect routes with paths descending from the route path exists' do - let!(:redirect2) { route.create_redirect("#{route.path}/foo") } - let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") } - let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") } - let!(:other_redirect) { route.create_redirect("other") } + context 'when the redirect route is differently cased' do + let!(:redirect1) { route.create_redirect(route.path.upcase) } - it 'deletes all redirects with paths that descend from the route path' do - route.delete_conflicting_redirects - expect(route.conflicting_redirects).to be_empty + it 'deletes the redirect' do + expect do + route.delete_conflicting_redirects + end.to change { RedirectRoute.count }.by(-1) end end end end describe '#conflicting_redirects' do + it 'returns an ActiveRecord::Relation' do + expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) + end + context 'when a redirect route with the same path exists' do - let!(:redirect1) { route.create_redirect(route.path) } + context 'when the redirect route has matching case' do + let!(:redirect1) { route.create_redirect(route.path) } - it 'returns the redirect route' do - expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) - expect(route.conflicting_redirects).to match_array([redirect1]) + it 'returns the redirect route' do + expect(route.conflicting_redirects).to match_array([redirect1]) + end + + context 'when redirect routes with paths descending from the route path exists' do + let!(:redirect2) { route.create_redirect("#{route.path}/foo") } + let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") } + let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") } + let!(:other_redirect) { route.create_redirect("other") } + + it 'returns the redirect routes' do + expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4]) + end + end end - context 'when redirect routes with paths descending from the route path exists' do - let!(:redirect2) { route.create_redirect("#{route.path}/foo") } - let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") } - let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") } - let!(:other_redirect) { route.create_redirect("other") } + context 'when the redirect route is differently cased' do + let!(:redirect1) { route.create_redirect(route.path.upcase) } - it 'returns the redirect routes' do - expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) - expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4]) + it 'returns the redirect route' do + expect(route.conflicting_redirects).to match_array([redirect1]) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0103fb6040e..97bb91a6ac8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -118,6 +118,17 @@ describe User do expect(user).to validate_uniqueness_of(:username).case_insensitive end + + context 'when username is changed' do + let(:user) { build_stubbed(:user, username: 'old_path', namespace: build_stubbed(:namespace)) } + + it 'validates move_dir is allowed for the namespace' do + expect(user.namespace).to receive(:any_project_has_container_registry_tags?).and_return(true) + user.username = 'new_path' + expect(user).to be_invalid + expect(user.errors.messages[:username].first).to match('cannot be changed if a personal project has container registry tags') + end + end end it { is_expected.to validate_presence_of(:projects_limit) } @@ -1280,7 +1291,7 @@ describe User do let!(:project2) { create(:project, forked_from_project: project3) } let!(:project3) { create(:project) } let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } - let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) } + let!(:push_event) { create(:push_event, project: project1, author: subject) } let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) } before do @@ -1322,10 +1333,18 @@ describe User do subject { create(:user) } let!(:project1) { create(:project, :repository) } let!(:project2) { create(:project, :repository, forked_from_project: project1) } - let!(:push_data) do - Gitlab::DataBuilder::Push.build_sample(project2, subject) + + let!(:push_event) do + event = create(:push_event, project: project2, author: subject) + + create(:push_event_payload, + event: event, + commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + commit_count: 0, + ref: 'master') + + event end - let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) } before do project1.team << [subject, :master] @@ -1352,8 +1371,13 @@ describe User do expect(subject.recent_push(project1)).to eq(nil) expect(subject.recent_push(project2)).to eq(push_event) - push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject) - push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1) + push_event1 = create(:push_event, project: project1, author: subject) + + create(:push_event_payload, + event: push_event1, + commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + commit_count: 0, + ref: 'master') expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 8b62aa268d9..3c02e6302b4 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -44,8 +44,8 @@ describe API::CommitStatuses do expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) - json_response.sort_by!{ |status| status['id'] } - expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false]) + json_response.sort_by! { |status| status['id'] } + expect(json_response.map { |status| status['allow_failure'] }).to eq([true, false, false, false]) end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 992a6e8d76a..dafe3f466a2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -16,11 +16,13 @@ describe API::Commits do end describe 'GET /projects/:id/repository/commits' do - context 'authorized user' do + let(:route) { "/projects/#{project_id}/repository/commits" } + + shared_examples_for 'project commits' do it "returns project commits" do commit = project.repository.commit - get api("/projects/#{project_id}/repository/commits", user) + get api(route, current_user) expect(response).to have_http_status(200) expect(response).to match_response_schema('public_api/v4/commits') @@ -32,7 +34,7 @@ describe API::Commits do it 'include correct pagination headers' do commit_count = project.repository.count_commits(ref: 'master').to_s - get api("/projects/#{project_id}/repository/commits", user) + get api(route, current_user) expect(response).to include_pagination_headers expect(response.headers['X-Total']).to eq(commit_count) @@ -40,140 +42,151 @@ describe API::Commits do end end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project_id}/repository/commits") + context 'when unauthenticated', 'and project is public' do + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'project commits' + end - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } end end - context "since optional parameter" do - it "returns project commits since provided parameter" do - commits = project.repository.commits("master") - after = commits.second.created_at + context 'when authenticated', 'as a master' do + let(:current_user) { user } - get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + it_behaves_like 'project commits' - expect(json_response.size).to eq 2 - expect(json_response.first["id"]).to eq(commits.first.id) - expect(json_response.second["id"]).to eq(commits.second.id) - end + context "since optional parameter" do + it "returns project commits since provided parameter" do + commits = project.repository.commits("master") + after = commits.second.created_at - it 'include correct pagination headers' do - commits = project.repository.commits("master") - after = commits.second.created_at - commit_count = project.repository.count_commits(ref: 'master', after: after).to_s + get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) - get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + expect(json_response.size).to eq 2 + expect(json_response.first["id"]).to eq(commits.first.id) + expect(json_response.second["id"]).to eq(commits.second.id) + end - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eql('1') + it 'include correct pagination headers' do + commits = project.repository.commits("master") + after = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', after: after).to_s + + get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end - end - context "until optional parameter" do - it "returns project commits until provided parameter" do - commits = project.repository.commits("master") - before = commits.second.created_at + context "until optional parameter" do + it "returns project commits until provided parameter" do + commits = project.repository.commits("master") + before = commits.second.created_at - get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - if commits.size >= 20 - expect(json_response.size).to eq(20) - else - expect(json_response.size).to eq(commits.size - 1) - end + if commits.size >= 20 + expect(json_response.size).to eq(20) + else + expect(json_response.size).to eq(commits.size - 1) + end - expect(json_response.first["id"]).to eq(commits.second.id) - expect(json_response.second["id"]).to eq(commits.third.id) - end + expect(json_response.first["id"]).to eq(commits.second.id) + expect(json_response.second["id"]).to eq(commits.third.id) + end - it 'include correct pagination headers' do - commits = project.repository.commits("master") - before = commits.second.created_at - commit_count = project.repository.count_commits(ref: 'master', before: before).to_s + it 'include correct pagination headers' do + commits = project.repository.commits("master") + before = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', before: before).to_s - get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eql('1') + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end - end - context "invalid xmlschema date parameters" do - it "returns an invalid parameter error message" do - get api("/projects/#{project_id}/repository/commits?since=invalid-date", user) + context "invalid xmlschema date parameters" do + it "returns an invalid parameter error message" do + get api("/projects/#{project_id}/repository/commits?since=invalid-date", user) - expect(response).to have_http_status(400) - expect(json_response['error']).to eq('since is invalid') + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('since is invalid') + end end - end - context "path optional parameter" do - it "returns project commits matching provided path parameter" do - path = 'files/ruby/popen.rb' - commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + context "path optional parameter" do + it "returns project commits matching provided path parameter" do + path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s - get api("/projects/#{project_id}/repository/commits?path=#{path}", user) + get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - expect(json_response.size).to eq(3) - expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - end + expect(json_response.size).to eq(3) + expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + end - it 'include correct pagination headers' do - path = 'files/ruby/popen.rb' - commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + it 'include correct pagination headers' do + path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s - get api("/projects/#{project_id}/repository/commits?path=#{path}", user) + get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eql('1') + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end - end - context 'with pagination params' do - let(:page) { 1 } - let(:per_page) { 5 } - let(:ref_name) { 'master' } - let!(:request) do - get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) - end + context 'with pagination params' do + let(:page) { 1 } + let(:per_page) { 5 } + let(:ref_name) { 'master' } + let!(:request) do + get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) + end - it 'returns correct headers' do - commit_count = project.repository.count_commits(ref: ref_name).to_s + it 'returns correct headers' do + commit_count = project.repository.count_commits(ref: ref_name).to_s - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eq('1') - expect(response.headers['Link']).to match(/page=1&per_page=5/) - expect(response.headers['Link']).to match(/page=2&per_page=5/) - end + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eq('1') + expect(response.headers['Link']).to match(/page=1&per_page=5/) + expect(response.headers['Link']).to match(/page=2&per_page=5/) + end - context 'viewing the first page' do - it 'returns the first 5 commits' do - commit = project.repository.commit + context 'viewing the first page' do + it 'returns the first 5 commits' do + commit = project.repository.commit - expect(json_response.size).to eq(per_page) - expect(json_response.first['id']).to eq(commit.id) - expect(response.headers['X-Page']).to eq('1') + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('1') + end end - end - context 'viewing the third page' do - let(:page) { 3 } + context 'viewing the third page' do + let(:page) { 3 } - it 'returns the third 5 commits' do - commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + it 'returns the third 5 commits' do + commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first - expect(json_response.size).to eq(per_page) - expect(json_response.first['id']).to eq(commit.id) - expect(response.headers['X-Page']).to eq('3') + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('3') + end end end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index d032d72de9c..e497ec333a2 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -182,7 +182,7 @@ describe API::DeployKeys do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) expect(response).to have_http_status(204) - end.to change{ project.deploy_keys.count }.by(-1) + end.to change { project.deploy_keys.count }.by(-1) end it 'returns 404 Not Found with invalid ID' do diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 48db964d782..a23d28994ce 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -59,6 +59,34 @@ describe API::Events do expect(json_response.size).to eq(1) end + context 'when the list of events includes push events' do + let(:event) do + create(:push_event, author: user, project: private_project) + end + + let!(:payload) { create(:push_event_payload, event: event) } + let(:payload_hash) { json_response[0]['push_data'] } + + before do + get api("/users/#{user.id}/events?action=pushed", user) + end + + it 'responds with HTTP 200 OK' do + expect(response).to have_http_status(200) + end + + it 'includes the push payload as a Hash' do + expect(payload_hash).to be_an_instance_of(Hash) + end + + it 'includes the push payload details' do + expect(payload_hash['commit_count']).to eq(payload.commit_count) + expect(payload_hash['action']).to eq(payload.action) + expect(payload_hash['ref_type']).to eq(payload.ref_type) + expect(payload_hash['commit_to']).to eq(payload.commit_to) + end + end + context 'when there are multiple events from different projects' do let(:second_note) { create(:note_on_issue, project: create(:project)) } @@ -140,33 +168,28 @@ describe API::Events do end context 'when exists some events' do + let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') } + let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') } + before do - create_event(note1) - create_event(note2) create_event(merge_request1) end - let(:note1) { create(:note_on_merge_request, project: private_project, author: user) } - let(:note2) { create(:note_on_issue, project: private_project, author: user) } - let(:merge_request1) { create(:merge_request, state: 'closed', author: user, assignee: user, source_project: private_project, title: 'Test') } - let(:merge_request2) { create(:merge_request, state: 'closed', author: user, assignee: user, source_project: private_project, title: 'Test') } - it 'avoids N+1 queries' do control_count = ActiveRecord::QueryRecorder.new do - get api("/projects/#{private_project.id}/events", user) + get api("/projects/#{private_project.id}/events", user), target_type: :merge_request end.count create_event(merge_request2) expect do - get api("/projects/#{private_project.id}/events", user) + get api("/projects/#{private_project.id}/events", user), target_type: :merge_request end.not_to exceed_query_limit(control_count) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response[0]).to include('target_type' => 'MergeRequest', 'target_id' => merge_request2.id) - expect(json_response[1]).to include('target_type' => 'MergeRequest', 'target_id' => merge_request1.id) + expect(json_response.size).to eq(2) + expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id]) end def create_event(target) diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 55c998b13b8..ea97c556430 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -33,6 +33,15 @@ describe API::Files do expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") end + it 'returns json when file has txt extension' do + file_path = "bar%2Fbranch-test.txt" + + get api(route(file_path), current_user), params + + expect(response).to have_http_status(200) + expect(response.content_type).to eq('application/json') + end + it 'returns file by commit sha' do # This file is deleted on HEAD file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" @@ -220,6 +229,7 @@ describe API::Files do post api(route("new_file_with_author%2Etxt"), user), valid_params expect(response).to have_http_status(201) + expect(response.content_type).to eq('application/json') last_commit = project.repository.commit.raw expect(last_commit.author_email).to eq(author_email) expect(last_commit.author_name).to eq(author_name) diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index 402ea057cc5..2179790d098 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -88,7 +88,7 @@ describe API::GroupVariables do it 'creates variable' do expect do post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true - end.to change{group.variables.count}.by(1) + end.to change {group.variables.count}.by(1) expect(response).to have_http_status(201) expect(json_response['key']).to eq('TEST_VARIABLE_2') @@ -99,7 +99,7 @@ describe API::GroupVariables do it 'creates variable with optional attributes' do expect do post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' - end.to change{group.variables.count}.by(1) + end.to change {group.variables.count}.by(1) expect(response).to have_http_status(201) expect(json_response['key']).to eq('TEST_VARIABLE_2') @@ -110,7 +110,7 @@ describe API::GroupVariables do it 'does not allow to duplicate variable key' do expect do post api("/groups/#{group.id}/variables", user), key: variable.key, value: 'VALUE_2' - end.to change{group.variables.count}.by(0) + end.to change {group.variables.count}.by(0) expect(response).to have_http_status(400) end @@ -192,7 +192,7 @@ describe API::GroupVariables do delete api("/groups/#{group.id}/variables/#{variable.key}", user) expect(response).to have_http_status(204) - end.to change{group.variables.count}.by(-1) + end.to change {group.variables.count}.by(-1) end it 'responds with 404 Not Found if requesting non-existing variable' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index eba1db15da6..313c38cd29c 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -512,7 +512,7 @@ describe API::Groups do let(:project) { create(:project) } let(:project_path) { CGI.escape(project.full_path) } - before(:each) do + before do allow_any_instance_of(Projects::TransferService) .to receive(:execute).and_return(true) end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 7a1bd76af7a..d4006fe71a2 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -56,7 +56,7 @@ describe API::Helpers do end def doorkeeper_guard_returns(value) - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value } + allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value } end def error!(message, status, header) @@ -161,7 +161,7 @@ describe API::Helpers do describe "when authenticating using a user's private token" do it "returns nil for an invalid token" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } expect(current_user).to be_nil end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 8a2de23716f..e9c30dba8d4 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -181,13 +181,12 @@ describe API::Internal do describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do context "access granted" do - before do - project.team << [user, :developer] - Timecop.freeze + around do |example| + Timecop.freeze { example.run } end - after do - Timecop.return + before do + project.team << [user, :developer] end context 'with env passed as a JSON' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9eda6836ded..0db645863fb 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -31,7 +31,7 @@ describe API::MergeRequests do it 'returns authentication error' do get api('/merge_requests') - expect(response).to have_http_status(401) + expect(response).to have_gitlab_http_status(401) end end @@ -43,7 +43,7 @@ describe API::MergeRequests do it 'returns an array of all merge requests' do get api('/merge_requests', user), scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |mr| mr['id'] }) @@ -56,7 +56,7 @@ describe API::MergeRequests do get api('/merge_requests', user), scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |mr| mr['id'] }) @@ -68,7 +68,7 @@ describe API::MergeRequests do get api('/merge_requests', user2) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -79,7 +79,7 @@ describe API::MergeRequests do get api('/merge_requests', user), author_id: user2.id, scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -90,7 +90,7 @@ describe API::MergeRequests do get api('/merge_requests', user), assignee_id: user2.id, scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -101,7 +101,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), scope: 'assigned-to-me' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -112,7 +112,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), scope: 'created-by-me' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -125,7 +125,7 @@ describe API::MergeRequests do it "returns authentication error" do get api("/projects/#{project.id}/merge_requests") - expect(response).to have_http_status(401) + expect(response).to have_gitlab_http_status(401) end end @@ -145,7 +145,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -166,7 +166,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests using simple mode" do get api("/projects/#{project.id}/merge_requests?view=simple", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) expect(json_response).to be_an Array @@ -182,7 +182,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -192,7 +192,7 @@ describe API::MergeRequests do it "returns an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -202,7 +202,7 @@ describe API::MergeRequests do it "returns an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -212,7 +212,7 @@ describe API::MergeRequests do it "returns an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -222,7 +222,7 @@ describe API::MergeRequests do it 'returns merge_request by "iids" array' do get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid] - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq merge_request_closed.title @@ -232,14 +232,14 @@ describe API::MergeRequests do it 'matches V4 response schema' do get api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/merge_requests') end it 'returns an empty array if no issue matches milestone' do get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -247,7 +247,7 @@ describe API::MergeRequests do it 'returns an empty array if milestone does not exist' do get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -262,7 +262,7 @@ describe API::MergeRequests do it 'returns an array of merge requests matching state in milestone' do get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request_closed.id) @@ -271,7 +271,7 @@ describe API::MergeRequests do it 'returns an array of labeled merge requests' do get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) @@ -280,7 +280,7 @@ describe API::MergeRequests do it 'returns an array of labeled merge requests where all labels match' do get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -288,7 +288,7 @@ describe API::MergeRequests do it 'returns an empty array if no merge request matches labels' do get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -307,7 +307,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(mr2.id) @@ -322,44 +322,44 @@ describe API::MergeRequests do it "returns an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end it "returns an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end it "returns an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } + response_dates = json_response.map { |merge_request| merge_request['updated_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end it "returns an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end end @@ -370,7 +370,7 @@ describe API::MergeRequests do it 'exposes known attributes' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['id']).to eq(merge_request.id) expect(json_response['iid']).to eq(merge_request.iid) expect(json_response['project_id']).to eq(merge_request.project.id) @@ -398,7 +398,7 @@ describe API::MergeRequests do it "returns merge_request" do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) expect(json_response['work_in_progress']).to eq(false) @@ -409,13 +409,13 @@ describe API::MergeRequests do it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns a 404 error if merge_request `id` is used instead of iid" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end context 'Work in Progress' do @@ -423,7 +423,7 @@ describe API::MergeRequests do it "returns merge_request" do get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['work_in_progress']).to eq(true) end end @@ -434,7 +434,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) commit = merge_request.commits.first - expect(response.status).to eq 200 + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.commits.size) @@ -444,13 +444,13 @@ describe API::MergeRequests do it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/commits", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns a 404 when merge_request id is used instead of iid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -458,19 +458,19 @@ describe API::MergeRequests do it 'returns the change information of the merge_request' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) - expect(response.status).to eq 200 + expect(response).to have_gitlab_http_status(200) expect(json_response['changes'].size).to eq(merge_request.diffs.size) end it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/changes", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns a 404 when merge_request id is used instead of iid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -485,7 +485,7 @@ describe API::MergeRequests do labels: 'label, label2', milestone_id: milestone.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) @@ -495,25 +495,25 @@ describe API::MergeRequests do it "returns 422 when source_branch equals target_branch" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "master", target_branch: "master", author: user - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it "returns 400 when source_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", target_branch: "master", author: user - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "markdown", author: user - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do post api("/projects/#{project.id}/merge_requests", user), target_branch: 'master', source_branch: 'markdown' - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it 'allows special label names' do @@ -523,7 +523,7 @@ describe API::MergeRequests do target_branch: 'master', author: user, labels: 'label, label?, label&foo, ?, &' - expect(response.status).to eq(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['labels']).to include 'label' expect(json_response['labels']).to include 'label?' expect(json_response['labels']).to include 'label&foo' @@ -549,7 +549,7 @@ describe API::MergeRequests do target_branch: 'master', author: user end.to change { MergeRequest.count }.by(0) - expect(response).to have_http_status(409) + expect(response).to have_gitlab_http_status(409) end end @@ -580,15 +580,17 @@ describe API::MergeRequests do let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } - before :each do |each| - fork_project.team << [user2, :reporter] + before do + fork_project.add_reporter(user2) + + allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['description']).to eq('Test description for Test merge_request') end @@ -599,7 +601,7 @@ describe API::MergeRequests do expect(fork_project.forked_from_project).to eq(project) post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') end @@ -613,25 +615,25 @@ describe API::MergeRequests do author: user2, target_project_id: project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it "returns 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do @@ -642,7 +644,7 @@ describe API::MergeRequests do source_branch: 'markdown', author: user, target_project_id: fork_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it 'returns 422 if targeting a different fork' do @@ -652,14 +654,14 @@ describe API::MergeRequests do source_branch: 'markdown', author: user2, target_project_id: unrelated_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end end it "returns 201 when target_branch is specified and for the same project" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) end end end @@ -674,7 +676,7 @@ describe API::MergeRequests do it "denies the deletion of the merge request" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end @@ -682,19 +684,19 @@ describe API::MergeRequests do it "destroys the merge request owners can destroy" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) - expect(response).to have_http_status(204) + expect(response).to have_gitlab_http_status(204) end it "returns 404 for an invalid merge request IID" do delete api("/projects/#{project.id}/merge_requests/12345", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end end @@ -705,7 +707,7 @@ describe API::MergeRequests do it "returns merge_request in case of success" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) end it "returns 406 if branch can't be merged" do @@ -714,21 +716,21 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(406) + expect(response).to have_gitlab_http_status(406) expect(json_response['message']).to eq('Branch cannot be merged') end it "returns 405 if merge_request is not open" do merge_request.close put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(405) + expect(response).to have_gitlab_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "returns 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(405) + expect(response).to have_gitlab_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -737,7 +739,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(405) + expect(response).to have_gitlab_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -745,21 +747,21 @@ describe API::MergeRequests do user2 = create(:user) project.team << [user2, :reporter] put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2) - expect(response).to have_http_status(401) + expect(response).to have_gitlab_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end it "returns 409 if the SHA parameter doesn't match" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse - expect(response).to have_http_status(409) + expect(response).to have_gitlab_http_status(409) expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') end it "succeeds if the SHA parameter matches" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) end it "enables merge when pipeline succeeds if the pipeline is active" do @@ -768,7 +770,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq('Test') expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end @@ -780,7 +782,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq('Test') expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end @@ -788,13 +790,13 @@ describe API::MergeRequests do it "returns 404 for an invalid merge request IID" do put api("/projects/#{project.id}/merge_requests/12345/merge", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -803,39 +805,39 @@ describe API::MergeRequests do it "returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['state']).to eq('closed') end end it "updates title and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq('New title') end it "updates description and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['description']).to eq('New description') end it "updates milestone_id and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['milestone']['id']).to eq(milestone.id) end it "returns merge_request with renamed target_branch" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['target_branch']).to eq('wiki') end it "returns merge_request that removes the source branch" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['force_remove_source_branch']).to be_truthy end @@ -856,7 +858,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil merge_request.reload - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) expect(merge_request.state).to eq('opened') end @@ -864,20 +866,20 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil merge_request.reload - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) expect(merge_request.state).to eq('opened') end it "returns 404 for an invalid merge request IID" do put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close" - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -890,7 +892,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -900,7 +902,7 @@ describe API::MergeRequests do it 'returns an empty array when there are no issues to be closed' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) @@ -916,7 +918,7 @@ describe API::MergeRequests do get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -936,19 +938,19 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end it "returns 404 for an invalid merge request IID" do get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -956,26 +958,26 @@ describe API::MergeRequests do it 'subscribes to a merge request' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin) - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user) - expect(response).to have_http_status(304) + expect(response).to have_gitlab_http_status(304) end it 'returns 404 if the merge request is not found' do post api("/projects/#{project.id}/merge_requests/123/subscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 404 if the merge request id is used instead of iid' do post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 403 if user has no access to read code' do @@ -984,7 +986,7 @@ describe API::MergeRequests do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end @@ -992,26 +994,26 @@ describe API::MergeRequests do it 'unsubscribes from a merge request' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user) - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin) - expect(response).to have_http_status(304) + expect(response).to have_gitlab_http_status(304) end it 'returns 404 if the merge request is not found' do post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 404 if the merge request id is used instead of iid' do post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 403 if user has no access to read code' do @@ -1020,7 +1022,7 @@ describe API::MergeRequests do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb index 9ff2b782b52..1fc0ec528b9 100644 --- a/spec/requests/api/pipeline_schedules_spec.rb +++ b/spec/requests/api/pipeline_schedules_spec.rb @@ -53,7 +53,7 @@ describe API::PipelineSchedules do it 'returns matched pipeline schedules' do get api("/projects/#{project.id}/pipeline_schedules", developer), scope: target - expect(json_response.map{ |r| r['active'] }).to all(eq(active?(target))) + expect(json_response.map { |r| r['active'] }).to all(eq(active?(target))) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9baac12821f..a89a58ff713 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -8,8 +8,8 @@ describe API::Projects do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } + let(:project) { create(:project, namespace: user.namespace) } + let(:project2) { create(:project, path: 'project2', namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } @@ -1065,6 +1065,14 @@ describe API::Projects do expect(project_fork_target.forked?).to be_truthy end + it 'refreshes the forks count cachce' do + expect(project_fork_source.forks_count).to be_zero + + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + + expect(project_fork_source.forks_count).to eq(1) + end + it 'fails if forked_from project which does not exist' do post api("/projects/#{project_fork_target.id}/fork/9999", admin) expect(response).to have_http_status(404) diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index e4f9c47fb33..1aa8a95780e 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -96,7 +96,7 @@ describe API::ProtectedBranches do describe 'POST /projects/:id/protected_branches' do let(:branch_name) { 'new_branch' } - context 'when authenticated as a master' do + context 'when authenticated as a master' do before do project.add_master(user) end @@ -221,7 +221,7 @@ describe API::ProtectedBranches do context 'when branch has a wildcard in its name' do let(:protected_name) { 'feature*' } - + it "unprotects a wildcard branch" do delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index edd6516cf34..e9ee3dd679d 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -683,7 +683,7 @@ describe API::Runner do end context 'when job has been updated recently' do - it { expect{ patch_the_trace }.not_to change { job.updated_at }} + it { expect { patch_the_trace }.not_to change { job.updated_at }} it "changes the job's trace" do patch_the_trace @@ -692,7 +692,7 @@ describe API::Runner do end context 'when Runner makes a force-patch' do - it { expect{ force_patch_the_trace }.not_to change { job.updated_at }} + it { expect { force_patch_the_trace }.not_to change { job.updated_at }} it "doesn't change the build.trace" do force_patch_the_trace diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 3a95db030d4..c8ff25f70fa 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -36,7 +36,7 @@ describe API::Runners do it 'returns user available runners' do get api('/runners', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any? { |r| r['is_shared'] } expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array @@ -46,7 +46,7 @@ describe API::Runners do it 'filters runners by scope' do get api('/runners?scope=active', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any? { |r| r['is_shared'] } expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array @@ -74,7 +74,7 @@ describe API::Runners do it 'returns all runners' do get api('/runners/all', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any? { |r| r['is_shared'] } expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array @@ -93,7 +93,7 @@ describe API::Runners do it 'filters runners by scope' do get api('/runners/all?scope=specific', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any? { |r| r['is_shared'] } expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array @@ -277,7 +277,7 @@ describe API::Runners do delete api("/runners/#{shared_runner.id}", admin) expect(response).to have_http_status(204) - end.to change{ Ci::Runner.shared.count }.by(-1) + end.to change { Ci::Runner.shared.count }.by(-1) end end @@ -287,7 +287,7 @@ describe API::Runners do delete api("/runners/#{unused_specific_runner.id}", admin) expect(response).to have_http_status(204) - end.to change{ Ci::Runner.specific.count }.by(-1) + end.to change { Ci::Runner.specific.count }.by(-1) end it 'deletes used runner' do @@ -295,7 +295,7 @@ describe API::Runners do delete api("/runners/#{specific_runner.id}", admin) expect(response).to have_http_status(204) - end.to change{ Ci::Runner.specific.count }.by(-1) + end.to change { Ci::Runner.specific.count }.by(-1) end end @@ -330,7 +330,7 @@ describe API::Runners do delete api("/runners/#{specific_runner.id}", user) expect(response).to have_http_status(204) - end.to change{ Ci::Runner.specific.count }.by(-1) + end.to change { Ci::Runner.specific.count }.by(-1) end end end @@ -349,7 +349,7 @@ describe API::Runners do it "returns project's runners" do get api("/projects/#{project.id}/runners", user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any? { |r| r['is_shared'] } expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array @@ -385,14 +385,14 @@ describe API::Runners do it 'enables specific runner' do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id - end.to change{ project.runners.count }.by(+1) + end.to change { project.runners.count }.by(+1) expect(response).to have_http_status(201) end it 'avoids changes when enabling already enabled runner' do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id - end.to change{ project.runners.count }.by(0) + end.to change { project.runners.count }.by(0) expect(response).to have_http_status(409) end @@ -401,7 +401,7 @@ describe API::Runners do expect do post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id - end.to change{ project.runners.count }.by(0) + end.to change { project.runners.count }.by(0) expect(response).to have_http_status(403) end @@ -416,7 +416,7 @@ describe API::Runners do it 'enables any specific runner' do expect do post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id - end.to change{ project.runners.count }.by(+1) + end.to change { project.runners.count }.by(+1) expect(response).to have_http_status(201) end end @@ -461,7 +461,7 @@ describe API::Runners do delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) expect(response).to have_http_status(204) - end.to change{ project.runners.count }.by(-1) + end.to change { project.runners.count }.by(-1) end end @@ -469,7 +469,7 @@ describe API::Runners do it "does not disable project's runner" do expect do delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user) - end.to change{ project.runners.count }.by(0) + end.to change { project.runners.count }.by(0) expect(response).to have_http_status(403) end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index c3ed5cd8ece..737c028ad53 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -43,7 +43,9 @@ describe API::Settings, 'Settings' do default_artifacts_expire_in: '2 days', help_page_text: 'custom help text', help_page_hide_commercial_content: true, - help_page_support_url: 'http://example.com/help' + help_page_support_url: 'http://example.com/help', + project_export_enabled: false + expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['password_authentication_enabled']).to be_falsey @@ -58,6 +60,7 @@ describe API::Settings, 'Settings' do expect(json_response['help_page_text']).to eq('custom help text') expect(json_response['help_page_hide_commercial_content']).to be_truthy expect(json_response['help_page_support_url']).to eq('http://example.com/help') + expect(json_response['project_export_enabled']).to be_falsey end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 373fab4d98a..d09b8bc42f1 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -52,10 +52,10 @@ describe API::Snippets do expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, public_snippet_other.id) - expect(json_response.map{ |snippet| snippet['web_url']} ).to include( + expect(json_response.map { |snippet| snippet['web_url']} ).to include( "http://localhost/snippets/#{public_snippet.id}", "http://localhost/snippets/#{public_snippet_other.id}") - expect(json_response.map{ |snippet| snippet['raw_url']} ).to include( + expect(json_response.map { |snippet| snippet['raw_url']} ).to include( "http://localhost/snippets/#{public_snippet.id}/raw", "http://localhost/snippets/#{public_snippet_other.id}/raw") end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index d5c53b703dd..572e9a0fd07 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -185,7 +185,7 @@ describe API::Triggers do expect do post api("/projects/#{project.id}/triggers", user), description: 'trigger' - end.to change{project.triggers.count}.by(1) + end.to change {project.triggers.count}.by(1) expect(response).to have_http_status(201) expect(json_response).to include('description' => 'trigger') @@ -288,7 +288,7 @@ describe API::Triggers do delete api("/projects/#{project.id}/triggers/#{trigger.id}", user) expect(response).to have_http_status(204) - end.to change{project.triggers.count}.by(-1) + end.to change {project.triggers.count}.by(-1) end it 'responds with 404 Not Found if requesting non-existing trigger' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2dc7be22f8f..49739a1601a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -217,9 +217,19 @@ describe API::Users do it "does not return the user's `is_admin` flag" do get api("/users/#{user.id}", user) + expect(response).to have_http_status(200) expect(json_response['is_admin']).to be_nil end + context 'when authenticated as admin' do + it 'includes the `is_admin` field' do + get api("/users/#{user.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['is_admin']).to be(false) + end + end + context 'for an anonymous user' do it "returns a user by id" do get api("/users/#{user.id}") diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb index 13a62423b1d..2affd0cfa51 100644 --- a/spec/requests/api/v3/deploy_keys_spec.rb +++ b/spec/requests/api/v3/deploy_keys_spec.rb @@ -87,7 +87,7 @@ describe API::V3::DeployKeys do expect do post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs - end.to change{ project.deploy_keys.count }.by(1) + end.to change { project.deploy_keys.count }.by(1) end it 'returns an existing ssh key when attempting to add a duplicate' do @@ -122,7 +122,7 @@ describe API::V3::DeployKeys do it 'should delete existing key' do expect do delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) - end.to change{ project.deploy_keys.count }.by(-1) + end.to change { project.deploy_keys.count }.by(-1) end it 'should return 404 Not Found with invalid ID' do diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index 10756e494c3..778fcc73c30 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -504,7 +504,7 @@ describe API::V3::Groups do let(:project) { create(:project) } let(:project_path) { CGI.escape(project.full_path) } - before(:each) do + before do allow_any_instance_of(Projects::TransferService) .to receive(:execute).and_return(true) end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index ef7516fc28f..86f38dd4ec1 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -90,7 +90,7 @@ describe API::MergeRequests do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end @@ -99,7 +99,7 @@ describe API::MergeRequests do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end @@ -108,7 +108,7 @@ describe API::MergeRequests do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } + response_dates = json_response.map { |merge_request| merge_request['updated_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end @@ -117,7 +117,7 @@ describe API::MergeRequests do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end end @@ -315,15 +315,17 @@ describe API::MergeRequests do let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } - before :each do |each| - fork_project.team << [user2, :reporter] + before do + fork_project.add_reporter(user2) + + allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['description']).to eq('Test description for Test merge_request') end @@ -334,7 +336,7 @@ describe API::MergeRequests do expect(fork_project.forked_from_project).to eq(project) post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') end @@ -348,25 +350,25 @@ describe API::MergeRequests do author: user2, target_project_id: project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it "returns 400 when source_branch is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do @@ -377,7 +379,7 @@ describe API::MergeRequests do source_branch: 'markdown', author: user, target_project_id: fork_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it 'returns 422 if targeting a different fork' do @@ -387,14 +389,14 @@ describe API::MergeRequests do source_branch: 'markdown', author: user2, target_project_id: unrelated_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end end it "returns 201 when target_branch is specified and for the same project" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) end end end diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb index 758fb482374..3963924a066 100644 --- a/spec/requests/api/v3/project_snippets_spec.rb +++ b/spec/requests/api/v3/project_snippets_spec.rb @@ -30,7 +30,7 @@ describe API::ProjectSnippets do expect(response).to have_http_status(200) expect(json_response.size).to eq(3) - expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) + expect(json_response.map { |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) expect(json_response.last).to have_key('web_url') end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index fca5b5b5d82..a514166274a 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -1004,6 +1004,14 @@ describe API::V3::Projects do expect(project_fork_target.forked?).to be_truthy end + it 'refreshes the forks count cachce' do + expect(project_fork_source.forks_count).to be_zero + + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + + expect(project_fork_source.forks_count).to eq(1) + end + it 'fails if forked_from project which does not exist' do post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) expect(response).to have_http_status(404) diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb index 78660afd840..a31eb3f1d43 100644 --- a/spec/requests/api/v3/runners_spec.rb +++ b/spec/requests/api/v3/runners_spec.rb @@ -38,7 +38,7 @@ describe API::V3::Runners do delete v3_api("/runners/#{shared_runner.id}", admin) expect(response).to have_http_status(200) - end.to change{ Ci::Runner.shared.count }.by(-1) + end.to change { Ci::Runner.shared.count }.by(-1) end end @@ -48,7 +48,7 @@ describe API::V3::Runners do delete v3_api("/runners/#{unused_specific_runner.id}", admin) expect(response).to have_http_status(200) - end.to change{ Ci::Runner.specific.count }.by(-1) + end.to change { Ci::Runner.specific.count }.by(-1) end it 'deletes used runner' do @@ -56,7 +56,7 @@ describe API::V3::Runners do delete v3_api("/runners/#{specific_runner.id}", admin) expect(response).to have_http_status(200) - end.to change{ Ci::Runner.specific.count }.by(-1) + end.to change { Ci::Runner.specific.count }.by(-1) end end @@ -91,7 +91,7 @@ describe API::V3::Runners do delete v3_api("/runners/#{specific_runner.id}", user) expect(response).to have_http_status(200) - end.to change{ Ci::Runner.specific.count }.by(-1) + end.to change { Ci::Runner.specific.count }.by(-1) end end end @@ -113,7 +113,7 @@ describe API::V3::Runners do delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) expect(response).to have_http_status(200) - end.to change{ project.runners.count }.by(-1) + end.to change { project.runners.count }.by(-1) end end @@ -121,7 +121,7 @@ describe API::V3::Runners do it "does not disable project's runner" do expect do delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user) - end.to change{ project.runners.count }.by(0) + end.to change { project.runners.count }.by(0) expect(response).to have_http_status(403) end end diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb index 1bc2258ebd3..9ead3cad8bb 100644 --- a/spec/requests/api/v3/snippets_spec.rb +++ b/spec/requests/api/v3/snippets_spec.rb @@ -45,10 +45,10 @@ describe API::V3::Snippets do expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, public_snippet_other.id) - expect(json_response.map{ |snippet| snippet['web_url']} ).to include( + expect(json_response.map { |snippet| snippet['web_url']} ).to include( "http://localhost/snippets/#{public_snippet.id}", "http://localhost/snippets/#{public_snippet_other.id}") - expect(json_response.map{ |snippet| snippet['raw_url']} ).to include( + expect(json_response.map { |snippet| snippet['raw_url']} ).to include( "http://localhost/snippets/#{public_snippet.id}/raw", "http://localhost/snippets/#{public_snippet_other.id}/raw") end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb index 60212660fb6..075de2c0cba 100644 --- a/spec/requests/api/v3/triggers_spec.rb +++ b/spec/requests/api/v3/triggers_spec.rb @@ -171,7 +171,7 @@ describe API::V3::Triggers do it 'creates trigger' do expect do post v3_api("/projects/#{project.id}/triggers", user) - end.to change{project.triggers.count}.by(1) + end.to change {project.triggers.count}.by(1) expect(response).to have_http_status(201) expect(json_response).to be_a(Hash) @@ -202,7 +202,7 @@ describe API::V3::Triggers do delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) expect(response).to have_http_status(200) - end.to change{project.triggers.count}.by(-1) + end.to change {project.triggers.count}.by(-1) end it 'responds with 404 Not Found if requesting non-existing trigger' do diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index bc0a4ab20a3..227b8d1b0c1 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -252,6 +252,31 @@ describe API::V3::Users do end context "as a user than can see the event's project" do + context 'when the list of events includes push events' do + let(:event) { create(:push_event, author: user, project: project) } + let!(:payload) { create(:push_event_payload, event: event) } + let(:payload_hash) { json_response[0]['push_data'] } + + before do + get api("/users/#{user.id}/events?action=pushed", user) + end + + it 'responds with HTTP 200 OK' do + expect(response).to have_http_status(200) + end + + it 'includes the push payload as a Hash' do + expect(payload_hash).to be_an_instance_of(Hash) + end + + it 'includes the push payload details' do + expect(payload_hash['commit_count']).to eq(payload.commit_count) + expect(payload_hash['action']).to eq(payload.action) + expect(payload_hash['ref_type']).to eq(payload.ref_type) + expect(payload_hash['commit_to']).to eq(payload.commit_to) + end + end + context 'joined event' do it 'returns the "joined" event' do get v3_api("/users/#{user.id}/events", user) diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 098a0d8ca7d..48592e12822 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -74,7 +74,7 @@ describe API::Variables do it 'creates variable' do expect do post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true - end.to change{project.variables.count}.by(1) + end.to change {project.variables.count}.by(1) expect(response).to have_http_status(201) expect(json_response['key']).to eq('TEST_VARIABLE_2') @@ -85,7 +85,7 @@ describe API::Variables do it 'creates variable with optional attributes' do expect do post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' - end.to change{project.variables.count}.by(1) + end.to change {project.variables.count}.by(1) expect(response).to have_http_status(201) expect(json_response['key']).to eq('TEST_VARIABLE_2') @@ -96,7 +96,7 @@ describe API::Variables do it 'does not allow to duplicate variable key' do expect do post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2' - end.to change{project.variables.count}.by(0) + end.to change {project.variables.count}.by(0) expect(response).to have_http_status(400) end @@ -166,7 +166,7 @@ describe API::Variables do delete api("/projects/#{project.id}/variables/#{variable.key}", user) expect(response).to have_http_status(204) - end.to change{project.variables.count}.by(-1) + end.to change {project.variables.count}.by(-1) end it 'responds with 404 Not Found if requesting non-existing variable' do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c077c458163..7ccba4ba3ec 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -130,7 +130,7 @@ describe Ci::API::Builds do register_builds info: { platform: :darwin } expect(response).to have_http_status(201) - + expect(json_response["options"]).to be_empty end end @@ -421,7 +421,7 @@ describe Ci::API::Builds do end context 'when build has been updated recently' do - it { expect{ patch_the_trace }.not_to change { build.updated_at }} + it { expect { patch_the_trace }.not_to change { build.updated_at }} it 'changes the build trace' do patch_the_trace @@ -430,7 +430,7 @@ describe Ci::API::Builds do end context 'when Runner makes a force-patch' do - it { expect{ force_patch_the_trace }.not_to change { build.updated_at }} + it { expect { force_patch_the_trace }.not_to change { build.updated_at }} it "doesn't change the build.trace" do force_patch_the_trace diff --git a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb new file mode 100644 index 00000000000..1006594499a --- /dev/null +++ b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/safer_boolean_column' + +describe RuboCop::Cop::Migration::SaferBooleanColumn do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + described_class::SMALL_TABLES.each do |table| + context "for the #{table} table" do + sources_and_offense = [ + ["add_column :#{table}, :column, :boolean, default: true", 'should disallow nulls'], + ["add_column :#{table}, :column, :boolean, default: false", 'should disallow nulls'], + ["add_column :#{table}, :column, :boolean, default: nil", 'should have a default and should disallow nulls'], + ["add_column :#{table}, :column, :boolean, null: false", 'should have a default'], + ["add_column :#{table}, :column, :boolean, null: true", 'should have a default and should disallow nulls'], + ["add_column :#{table}, :column, :boolean", 'should have a default and should disallow nulls'], + ["add_column :#{table}, :column, :boolean, default: nil, null: false", 'should have a default'], + ["add_column :#{table}, :column, :boolean, default: nil, null: true", 'should have a default and should disallow nulls'], + ["add_column :#{table}, :column, :boolean, default: false, null: true", 'should disallow nulls'] + ] + + sources_and_offense.each do |source, offense| + context "given the source \"#{source}\"" do + it "registers the offense matching \"#{offense}\"" do + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.first.message).to match(offense) + end + end + end + end + + inoffensive_sources = [ + "add_column :#{table}, :column, :boolean, default: true, null: false", + "add_column :#{table}, :column, :boolean, default: false, null: false" + ] + + inoffensive_sources.each do |source| + context "given the source \"#{source}\"" do + it "registers no offense" do + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses).to be_empty + end + end + end + end + end + end + + it 'registers no offense for tables not listed in SMALL_TABLES' do + inspect_source(cop, "add_column :large_table, :column, :boolean") + + expect(cop.offenses).to be_empty + end + + it 'registers no offense for non-boolean columns' do + table = described_class::SMALL_TABLES.sample + inspect_source(cop, "add_column :#{table}, :column, :string") + + expect(cop.offenses).to be_empty + end + end + + context 'outside of migration' do + it 'registers no offense' do + table = described_class::SMALL_TABLES.sample + inspect_source(cop, "add_column :#{table}, :column, :boolean") + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 86e703a6448..1ff4908972a 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -13,12 +13,8 @@ describe AnalyticsBuildEntity do subject { entity.as_json } - before do - Timecop.freeze - end - - after do - Timecop.return + around do |example| + Timecop.freeze { example.run } end it 'contains the URL' do @@ -47,7 +43,7 @@ describe AnalyticsBuildEntity do let(:finished_at) { nil } it 'does not blow up' do - expect{ subject[:date] }.not_to raise_error + expect { subject[:date] }.not_to raise_error end it 'shows the right message' do @@ -63,7 +59,7 @@ describe AnalyticsBuildEntity do let(:started_at) { nil } it 'does not blow up' do - expect{ subject[:date] }.not_to raise_error + expect { subject[:date] }.not_to raise_error end it 'shows the right message' do @@ -79,7 +75,7 @@ describe AnalyticsBuildEntity do let(:finished_at) { nil } it 'does not blow up' do - expect{ subject[:date] }.not_to raise_error + expect { subject[:date] }.not_to raise_error end it 'shows the right message' do diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 362d754bca3..2de8daba6b5 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -111,7 +111,7 @@ describe PipelineSerializer do shared_examples 'no N+1 queries' do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(1).of(59) + expect(recorded.count).to be_within(1).of(57) expect(recorded.cached_count).to eq(0) end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 730df4e0336..53d4fcfed18 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -66,7 +66,7 @@ describe Ci::CreatePipelineService do context 'when there is no pipeline for source branch' do it "does not update merge request head pipeline" do - merge_request = create(:merge_request, source_branch: 'other_branch', target_branch: "branch_1", source_project: project) + merge_request = create(:merge_request, source_branch: 'feature', target_branch: "branch_1", source_project: project) head_pipeline = pipeline diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 049b082277a..08267d6e6a0 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -20,6 +20,10 @@ describe CreateDeploymentService do let(:service) { described_class.new(job) } + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + describe '#execute' do subject { service.execute } diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 42adb044190..02d7ddeb86b 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -117,12 +117,52 @@ describe EventCreateService do let(:project) { create(:project) } let(:user) { create(:user) } + let(:push_data) do + { + commits: [ + { + id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + message: 'This is a commit' + } + ], + before: '0000000000000000000000000000000000000000', + after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + total_commits_count: 1, + ref: 'refs/heads/my-branch' + } + end + it 'creates a new event' do - expect { service.push(project, user, {}) }.to change { Event.count } + expect { service.push(project, user, push_data) }.to change { Event.count } + end + + it 'creates the push event payload' do + expect(PushEventPayloadService).to receive(:new) + .with(an_instance_of(PushEvent), push_data) + .and_call_original + + service.push(project, user, push_data) end it 'updates user last activity' do - expect { service.push(project, user, {}) }.to change { user_activity(user) } + expect { service.push(project, user, push_data) } + .to change { user_activity(user) } + end + + it 'does not create any event data when an error is raised' do + payload_service = double(:service) + + allow(payload_service).to receive(:execute) + .and_raise(RuntimeError) + + allow(PushEventPayloadService).to receive(:new) + .and_return(payload_service) + + expect { service.push(project, user, push_data) } + .to raise_error(RuntimeError) + + expect(Event.count).to eq(0) + expect(PushEventPayload.count).to eq(0) end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 82724ccd281..e3c1bdce300 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -132,7 +132,7 @@ describe GitPushService, services: true do end it "creates a new pipeline" do - expect{ subject }.to change{ Ci::Pipeline.count } + expect { subject }.to change { Ci::Pipeline.count } expect(Ci::Pipeline.last).to be_push end end @@ -141,10 +141,13 @@ describe GitPushService, services: true do let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } let(:event) { Event.find_by_action(Event::PUSHED) } - it { expect(event).not_to be_nil } + it { expect(event).to be_an_instance_of(PushEvent) } it { expect(event.project).to eq(project) } it { expect(event.action).to eq(Event::PUSHED) } - it { expect(event.data).to eq(push_data) } + it { expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) } + it { expect(event.push_event_payload.commit_from).to eq(oldrev) } + it { expect(event.push_event_payload.commit_to).to eq(newrev) } + it { expect(event.push_event_payload.ref).to eq('master') } context "Updates merge requests" do it "when pushing a new branch for the first time" do @@ -685,10 +688,38 @@ describe GitPushService, services: true do ) end - it 'calls CreateGpgSignatureWorker.perform_async for each commit' do - expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id) + context 'when the commit has a signature' do + context 'when the signature is already cached' do + before do + create(:gpg_signature, commit_sha: sample_commit.id) + end - execute_service(project, user, oldrev, newrev, ref) + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end + + context 'when the signature is not yet cached' do + it 'queues a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end + end + + context 'when the commit does not have a signature' do + before do + allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([]) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index f877c145390..05695aa8188 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -39,7 +39,7 @@ describe GitTagPushService do end it "creates a new pipeline" do - expect{ subject }.to change{ Ci::Pipeline.count } + expect { subject }.to change { Ci::Pipeline.count } expect(Ci::Pipeline.last).to be_push end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index befa65ec0f8..bdaab88d673 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -118,7 +118,7 @@ describe Issuable::BulkUpdateService do context 'when the new assignee ID is not present' do it 'does not unassign' do expect { bulk_update(issue, assignee_ids: []) } - .not_to change{ issue.reload.assignees } + .not_to change { issue.reload.assignees } end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index fcbd69fd58b..9b2d9e79f4f 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -351,14 +351,14 @@ describe Issues::CreateService do end it 'marks related spam_log as recaptcha_verified' do - expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true) + expect { issue }.to change {SpamLog.last.recaptcha_verified}.from(false).to(true) end context 'when spam log does not belong to a user' do let(:log_user) { create(:user) } it 'does not mark spam_log as recaptcha_verified' do - expect { issue }.not_to change{SpamLog.last.recaptcha_verified} + expect { issue }.not_to change {SpamLog.last.recaptcha_verified} end end end @@ -378,7 +378,7 @@ describe Issues::CreateService do end it 'creates a new spam_log' do - expect{issue}.to change{SpamLog.count}.from(0).to(1) + expect {issue}.to change {SpamLog.count}.from(0).to(1) end it 'assigns a spam_log to an issue' do diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index fac66791ffb..67a86c50fc1 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -20,7 +20,7 @@ describe Issues::ResolveDiscussions do describe "for resolving discussions" do let(:discussion) { create(:diff_note_on_merge_request, project: project, note: "Almost done").to_discussion } let(:merge_request) { discussion.noteable } - let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") } + let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") } describe "#merge_request_for_resolving_discussion" do let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 814e2cfbed0..34fb16edc84 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -478,7 +478,7 @@ describe Issues::UpdateService, :mailer do feature_visibility_attr = :"#{issue.model_name.plural}_access_level" project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) - expect{ update_issue(assignee_ids: [assignee.id]) }.not_to change{ issue.assignees } + expect { update_issue(assignee_ids: [assignee.id]) }.not_to change { issue.assignees } end end end diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb index bb95fe20fbf..c3fe33045fa 100644 --- a/spec/services/labels/update_service_spec.rb +++ b/spec/services/labels/update_service_spec.rb @@ -13,7 +13,7 @@ describe Labels::UpdateService do let(:expected_saved_color) { hex_color } - before(:each) do + before do @label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project) expect(@label).to be_persisted end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 8fef480274d..a1f3bec42cc 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -48,6 +48,16 @@ describe MergeRequests::CreateService do expect(Todo.where(attributes).count).to be_zero end + it 'creates exactly 1 create MR event' do + attributes = { + action: Event::CREATED, + target_id: @merge_request.id, + target_type: @merge_request.class.name + } + + expect(Event.where(attributes).count).to eq(1) + end + context 'when merge request is assigned to someone' do let(:opts) do { diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 672d86e4028..25599dea19f 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } - let(:source_branch) { "my_branch" } + let(:source_branch) { "merge-test" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } @@ -111,9 +111,9 @@ describe MergeRequests::GetUrlsService do end context 'pushing new branch and existing branch (with merge request created) at once' do - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "markdown") } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } - let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } @@ -124,7 +124,7 @@ describe MergeRequests::GetUrlsService do url: new_merge_request_url, new_merge_request: true }, { - branch_name: "existing_branch", + branch_name: "markdown", url: show_merge_request_url, new_merge_request: false }]) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 9368594bc86..681feee61d1 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -509,7 +509,7 @@ describe MergeRequests::UpdateService, :mailer do feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level" project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) - expect{ update_merge_request(assignee_id: assignee) }.not_to change{ merge_request.assignee } + expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee } end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 6af5c79135d..44b2d28d1d4 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -4,7 +4,7 @@ describe NotificationService, :mailer do let(:notification) { described_class.new } let(:assignee) { create(:user) } - around(:each) do |example| + around do |example| perform_enqueued_jobs do example.run end @@ -80,12 +80,16 @@ describe NotificationService, :mailer do describe 'Keys' do describe '#new_key' do - let!(:key) { create(:personal_key) } + let(:key_options) { {} } + let!(:key) { create(:personal_key, key_options) } it { expect(notification.new_key(key)).to be_truthy } + it { should_email(key.user) } - it 'sends email to key owner' do - expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + describe 'never emails the ghost user' do + let(:key_options) { { user: User.ghost } } + + it { should_not_email_anyone } end end end @@ -97,7 +101,7 @@ describe NotificationService, :mailer do it { expect(notification.new_gpg_key(key)).to be_truthy } it 'sends email to key owner' do - expect{ notification.new_gpg_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + expect { notification.new_gpg_key(key) }.to change { ActionMailer::Base.deliveries.size }.by(1) end end end @@ -109,7 +113,7 @@ describe NotificationService, :mailer do it { expect(notification.new_email(email)).to be_truthy } it 'sends email to email owner' do - expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + expect { notification.new_email(email) }.to change { ActionMailer::Base.deliveries.size }.by(1) end end end @@ -1173,19 +1177,39 @@ describe NotificationService, :mailer do end end - describe '#project_exported' do - it do - notification.project_exported(project, @u_disabled) + context 'user with notifications disabled' do + describe '#project_exported' do + it do + notification.project_exported(project, @u_disabled) + + should_not_email_anyone + end + end + + describe '#project_not_exported' do + it do + notification.project_not_exported(project, @u_disabled, ['error']) - should_only_email(@u_disabled) + should_not_email_anyone + end end end - describe '#project_not_exported' do - it do - notification.project_not_exported(project, @u_disabled, ['error']) + context 'user with notifications enabled' do + describe '#project_exported' do + it do + notification.project_exported(project, @u_participating) - should_only_email(@u_disabled) + should_only_email(@u_participating) + end + end + + describe '#project_not_exported' do + it do + notification.project_not_exported(project, @u_participating, ['error']) + + should_only_email(@u_participating) + end end end end @@ -1196,7 +1220,7 @@ describe NotificationService, :mailer do let(:group) { create(:group) } let(:member) { create(:user) } - before(:each) do + before do group.add_owner(creator) group.add_developer(member, creator) end @@ -1209,6 +1233,35 @@ describe NotificationService, :mailer do end.to change { ActionMailer::Base.deliveries.size }.by(1) end end + + describe '#new_group_member' do + let(:group) { create(:group) } + let(:added_user) { create(:user) } + + def create_member! + GroupMember.create( + group: group, + user: added_user, + access_level: Gitlab::Access::GUEST + ) + end + + it 'sends a notification' do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before do + create_global_setting_for(added_user, :disabled) + end + + it 'does not send a notification' do + create_member! + should_not_email_anyone + end + end + end end describe 'ProjectMember' do @@ -1216,7 +1269,7 @@ describe NotificationService, :mailer do let(:project) { create(:project) } let(:member) { create(:user) } - before(:each) do + before do project.add_developer(member, current_user: project.owner) end @@ -1228,6 +1281,31 @@ describe NotificationService, :mailer do end.to change { ActionMailer::Base.deliveries.size }.by(1) end end + + describe '#new_project_member' do + let(:project) { create(:project) } + let(:added_user) { create(:user) } + + def create_member! + create(:project_member, user: added_user, project: project) + end + + it do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before do + create_global_setting_for(added_user, :disabled) + end + + it do + create_member! + should_not_email_anyone + end + end + end end context 'guest user in private project' do diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 85b05ef6d05..c867139d1de 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -142,13 +142,13 @@ describe Projects::DestroyService do context 'when `execute` raises unexpected error' do before do expect_any_instance_of(Project) - .to receive(:destroy!).and_raise(Exception.new("Other error message")) + .to receive(:destroy!).and_raise(Exception.new('Other error message')) end it 'allows error to bubble up and rolls back project deletion' do expect do Sidekiq::Testing.inline! { destroy_project(project, user, {}) } - end.to raise_error + end.to raise_error(Exception, 'Other error message') expect(project.reload.pending_delete).to be(false) expect(project.delete_error).to include("Other error message") diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index c90536ba346..21c4b30734c 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -50,6 +50,14 @@ describe Projects::ForkService do expect(@from_project.avatar.file).to be_exists end + + it 'flushes the forks count cache of the source project' do + expect(@from_project.forks_count).to be_zero + + fork_project(@from_project, @to_user) + + expect(@from_project.forks_count).to eq(1) + end end end diff --git a/spec/services/projects/forks_count_service_spec.rb b/spec/services/projects/forks_count_service_spec.rb new file mode 100644 index 00000000000..cf299c5d09b --- /dev/null +++ b/spec/services/projects/forks_count_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Projects::ForksCountService do + let(:project) { build(:project, id: 42) } + let(:service) { described_class.new(project) } + + describe '#count' do + it 'returns the number of forks' do + allow(service).to receive(:uncached_count).and_return(1) + + expect(service.count).to eq(1) + end + + it 'caches the forks count', :use_clean_rails_memory_store_caching do + expect(service).to receive(:uncached_count).once.and_return(1) + + 2.times { service.count } + end + end + + describe '#refresh_cache', :use_clean_rails_memory_store_caching do + it 'refreshes the cache' do + expect(service).to receive(:uncached_count).once.and_return(1) + + service.refresh_cache + + expect(service.count).to eq(1) + end + end + + describe '#delete_cache', :use_clean_rails_memory_store_caching do + it 'removes the cache' do + expect(service).to receive(:uncached_count).twice.and_return(1) + + service.count + service.delete_cache + service.count + end + end +end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index ebed802708d..385f56e447f 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -24,7 +24,7 @@ describe Projects::HousekeepingService do end context 'when no lease can be obtained' do - before(:each) do + before do expect(subject).to receive(:try_obtain_lease).and_return(false) end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 2ae8d5f7c54..4f1ab697460 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -29,4 +29,14 @@ describe Projects::UnlinkForkService do subject.execute end + + it 'refreshes the forks count cache of the source project' do + source = fork_project.forked_from_project + + expect(source.forks_count).to eq(1) + + subject.execute + + expect(source.forks_count).to be_zero + end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index d945e0aa1f0..1b282e82187 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -101,6 +101,13 @@ describe Projects::UpdateService, '#execute' do expect(Project.find(project.id).default_branch).to eq 'feature' end + + it 'does not change a default branch' do + # The branch 'unexisted-branch' does not exist. + update_project(project, admin, default_branch: 'unexisted-branch') + + expect(Project.find(project.id).default_branch).to eq 'master' + end end context 'when updating a project that contains container images' do diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb index 5698101af54..9fa5983db66 100644 --- a/spec/services/protected_branches/update_service_spec.rb +++ b/spec/services/protected_branches/update_service_spec.rb @@ -19,7 +19,7 @@ describe ProtectedBranches::UpdateService do let(:user) { create(:user) } it "raises error" do - expect{ service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) end end end diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb index d333430088d..2ece4e3b07b 100644 --- a/spec/services/protected_tags/update_service_spec.rb +++ b/spec/services/protected_tags/update_service_spec.rb @@ -19,7 +19,7 @@ describe ProtectedTags::UpdateService do let(:user) { create(:user) } it "raises error" do - expect{ service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError) end end end diff --git a/spec/services/push_event_payload_service_spec.rb b/spec/services/push_event_payload_service_spec.rb new file mode 100644 index 00000000000..81956200bff --- /dev/null +++ b/spec/services/push_event_payload_service_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +describe PushEventPayloadService do + let(:event) { create(:push_event) } + + describe '#execute' do + let(:push_data) do + { + commits: [ + { + id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + message: 'This is a commit' + } + ], + before: '0000000000000000000000000000000000000000', + after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + total_commits_count: 1, + ref: 'refs/heads/my-branch' + } + end + + it 'creates a new PushEventPayload row' do + payload = described_class.new(event, push_data).execute + + expect(payload.commit_count).to eq(1) + expect(payload.action).to eq('created') + expect(payload.ref_type).to eq('branch') + expect(payload.commit_from).to be_nil + expect(payload.commit_to).to eq(push_data[:after]) + expect(payload.ref).to eq('my-branch') + expect(payload.commit_title).to eq('This is a commit') + expect(payload.event_id).to eq(event.id) + end + + it 'sets the push_event_payload association of the used event' do + payload = described_class.new(event, push_data).execute + + expect(event.push_event_payload).to eq(payload) + end + end + + describe '#commit_title' do + it 'returns nil if no commits were pushed' do + service = described_class.new(event, commits: []) + + expect(service.commit_title).to be_nil + end + + it 'returns a String limited to 70 characters' do + service = described_class.new(event, commits: [{ message: 'a' * 100 }]) + + expect(service.commit_title).to eq(('a' * 67) + '...') + end + + it 'does not truncate the commit message if it is shorter than 70 characters' do + service = described_class.new(event, commits: [{ message: 'Hello' }]) + + expect(service.commit_title).to eq('Hello') + end + + it 'includes the first line of a commit message if the message spans multiple lines' do + service = described_class + .new(event, commits: [{ message: "Hello\n\nworld" }]) + + expect(service.commit_title).to eq('Hello') + end + end + + describe '#commit_from_id' do + it 'returns nil when creating a new ref' do + service = described_class.new(event, before: Gitlab::Git::BLANK_SHA) + + expect(service.commit_from_id).to be_nil + end + + it 'returns the ID of the first commit when pushing to an existing ref' do + service = described_class.new(event, before: '123') + + expect(service.commit_from_id).to eq('123') + end + end + + describe '#commit_to_id' do + it 'returns nil when removing an existing ref' do + service = described_class.new(event, after: Gitlab::Git::BLANK_SHA) + + expect(service.commit_to_id).to be_nil + end + end + + describe '#commit_count' do + it 'returns the number of commits' do + service = described_class.new(event, total_commits_count: 1) + + expect(service.commit_count).to eq(1) + end + + it 'raises when the push data does not contain the commits count' do + service = described_class.new(event, {}) + + expect { service.commit_count }.to raise_error(KeyError) + end + end + + describe '#ref' do + it 'returns the name of the ref' do + service = described_class.new(event, ref: 'refs/heads/foo') + + expect(service.ref).to eq('refs/heads/foo') + end + + it 'raises when the push data does not contain the ref name' do + service = described_class.new(event, {}) + + expect { service.ref }.to raise_error(KeyError) + end + end + + describe '#revision_before' do + it 'returns the revision from before the push' do + service = described_class.new(event, before: 'foo') + + expect(service.revision_before).to eq('foo') + end + + it 'raises when the push data does not contain the before revision' do + service = described_class.new(event, {}) + + expect { service.revision_before }.to raise_error(KeyError) + end + end + + describe '#revision_after' do + it 'returns the revision from after the push' do + service = described_class.new(event, after: 'foo') + + expect(service.revision_after).to eq('foo') + end + + it 'raises when the push data does not contain the after revision' do + service = described_class.new(event, {}) + + expect { service.revision_after }.to raise_error(KeyError) + end + end + + describe '#trimmed_ref' do + it 'returns the ref name without its prefix' do + service = described_class.new(event, ref: 'refs/heads/foo') + + expect(service.trimmed_ref).to eq('foo') + end + end + + describe '#create?' do + it 'returns true when creating a new ref' do + service = described_class.new(event, before: Gitlab::Git::BLANK_SHA) + + expect(service.create?).to eq(true) + end + + it 'returns false when pushing to an existing ref' do + service = described_class.new(event, before: 'foo') + + expect(service.create?).to eq(false) + end + end + + describe '#remove?' do + it 'returns true when removing an existing ref' do + service = described_class.new(event, after: Gitlab::Git::BLANK_SHA) + + expect(service.remove?).to eq(true) + end + + it 'returns false pushing to an existing ref' do + service = described_class.new(event, after: 'foo') + + expect(service.remove?).to eq(false) + end + end + + describe '#action' do + it 'returns :created when creating a ref' do + service = described_class.new(event, before: Gitlab::Git::BLANK_SHA) + + expect(service.action).to eq(:created) + end + + it 'returns :removed when removing an existing ref' do + service = described_class.new(event, + before: '123', + after: Gitlab::Git::BLANK_SHA) + + expect(service.action).to eq(:removed) + end + + it 'returns :pushed when pushing to an existing ref' do + service = described_class.new(event, before: '123', after: '456') + + expect(service.action).to eq(:pushed) + end + end + + describe '#ref_type' do + it 'returns :tag for a tag' do + service = described_class.new(event, ref: 'refs/tags/1.2') + + expect(service.ref_type).to eq(:tag) + end + + it 'returns :branch for a branch' do + service = described_class.new(event, ref: 'refs/heads/master') + + expect(service.ref_type).to eq(:branch) + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 8f1eb4863d9..6d36affa9dc 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -35,7 +35,7 @@ describe SystemNoteService do context 'metadata' do it 'creates a new system note metadata record' do - expect { subject }.to change{ SystemNoteMetadata.count }.from(0).to(1) + expect { subject }.to change { SystemNoteMetadata.count }.from(0).to(1) end it 'creates a record correctly' do @@ -477,7 +477,7 @@ describe SystemNoteService do end it 'does not create a system note metadata record' do - expect { subject }.not_to change{ SystemNoteMetadata.count } + expect { subject }.not_to change { SystemNoteMetadata.count } end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 80d05451e09..a9b34a5258a 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -586,13 +586,13 @@ describe TodoService do it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author) - expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) + expect { service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) end it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do create(:todo, :directly_addressed, user: skipped, project: project, target: addressed_mr_assigned, author: author) - expect{ service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count) + expect { service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count) end context 'with a task list' do diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 79d90defd78..0726e135b20 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -15,7 +15,7 @@ describe WebHookService do let(:service_instance) { described_class.new(project_hook, data, 'push_hooks') } describe '#execute' do - before(:each) do + before do project.hooks << [project_hook] WebMock.stub_request(:post, project_hook.url) @@ -144,7 +144,7 @@ describe WebHookService do describe '#async_execute' do let(:system_hook) { create(:system_hook) } - + it 'enqueue WebHookWorker' do expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks') diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb index ac2c89b3ff9..25ddf932d42 100644 --- a/spec/simplecov_env.rb +++ b/spec/simplecov_env.rb @@ -36,18 +36,25 @@ module SimpleCovEnv track_files '{app,lib}/**/*.rb' add_filter '/vendor/ruby/' + add_filter 'app/controllers/sherlock/' add_filter 'config/initializers/' + add_filter 'db/fixtures/' + add_filter 'lib/gitlab/sidekiq_middleware/' + add_filter 'lib/system_check/' add_group 'Controllers', 'app/controllers' - add_group 'Models', 'app/models' - add_group 'Mailers', 'app/mailers' - add_group 'Helpers', 'app/helpers' - add_group 'Workers', %w(app/jobs app/workers) - add_group 'Libraries', 'lib' - add_group 'Services', 'app/services' - add_group 'Finders', 'app/finders' - add_group 'Uploaders', 'app/uploaders' - add_group 'Validators', 'app/validators' + add_group 'Finders', 'app/finders' + add_group 'Helpers', 'app/helpers' + add_group 'Libraries', 'lib' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Policies', 'app/policies' + add_group 'Presenters', 'app/presenters' + add_group 'Serializers', 'app/serializers' + add_group 'Services', 'app/services' + add_group 'Uploaders', 'app/uploaders' + add_group 'Validators', 'app/validators' + add_group 'Workers', %w(app/jobs app/workers) merge_timeout 365.days end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0ba6ed56314..c10197ff651 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,7 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' require 'rspec/retry' +require 'rspec-parameterized' rspec_profiling_is_configured = ENV['RSPEC_PROFILING_POSTGRES_URL'].present? || @@ -69,7 +70,14 @@ RSpec.configure do |config| config.raise_errors_for_deprecations! + if ENV['CI'] + # This includes the first try, i.e. tests will be run 4 times before failing. + config.default_retry_count = 4 + config.reporter.register_listener(RspecFlaky::Listener.new, :example_passed, :dump_summary) + end + config.before(:suite) do + Timecop.safe_mode = true TestEnv.init end @@ -97,12 +105,6 @@ RSpec.configure do |config| reset_delivered_emails! end - if ENV['CI'] - config.around(:each) do |ex| - ex.run_with_retry retry: 2 - end - end - config.around(:each, :use_clean_rails_memory_store_caching) do |example| caching_store = Rails.cache Rails.cache = ActiveSupport::Cache::MemoryStore.new @@ -130,17 +132,12 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end - config.before(:example, :migration) do - ActiveRecord::Migrator - .migrate(migrations_paths, previous_migration.version) - - reset_column_in_migration_models + config.before(:each, :migration) do + schema_migrate_down! end - config.after(:example, :migration) do - ActiveRecord::Migrator.migrate(migrations_paths) - - reset_column_in_migration_models + config.after(:context, :migration) do + schema_migrate_up! end config.around(:each, :nested_groups) do |example| diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index bf769225012..4bb5113957e 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -50,7 +50,7 @@ shared_examples_for 'group and project milestones' do |route_definition| expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) - expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) + expect(json_response.map { |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) end it 'does not return any milestone if none found' do diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 7f5769209bb..b0f520d08e8 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -20,7 +20,7 @@ RSpec.configure do |config| end config.before(:each, :migration) do - DatabaseCleaner.strategy = :truncation + DatabaseCleaner.strategy = :truncation, { cache_tables: false } end config.before(:each) do diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 27e079c01dd..cb483ae9a5a 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -7,15 +7,18 @@ shared_examples 'reportable note' do let(:more_actions_selector) { '.more-actions.dropdown' } let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) } + it 'has an edit button' do + expect(comment).to have_selector('.js-note-edit') + end + it 'has a `More actions` dropdown' do expect(comment).to have_selector(more_actions_selector) end - it 'dropdown has Edit, Report and Delete links' do + it 'dropdown has Report and Delete links' do dropdown = comment.find(more_actions_selector) open_dropdown(dropdown) - expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index d21c4324d9e..99b8b6b7ea4 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -54,8 +54,8 @@ module FilteredSearchHelpers # Iterates through each visual token inside # .tokens-container to make sure the correct names and values are rendered def expect_tokens(tokens) - page.find '.filtered-search-box .tokens-container' do - page.all(:css, '.tokens-container li').each_with_index do |el, index| + page.within '.filtered-search-box .tokens-container' do + page.all(:css, '.tokens-container li .selectable').each_with_index do |el, index| token_name = tokens[index][:name] token_value = tokens[index][:value] @@ -67,6 +67,28 @@ module FilteredSearchHelpers end end + def create_token(token_name, token_value = nil, symbol = nil) + { name: token_name, value: "#{symbol}#{token_value}" } + end + + def author_token(author_name = nil) + create_token('Author', author_name) + end + + def assignee_token(assignee_name = nil) + create_token('Assignee', assignee_name) + end + + def milestone_token(milestone_name = nil, has_symbol = true) + symbol = has_symbol ? '%' : nil + create_token('Milestone', milestone_name, symbol) + end + + def label_token(label_name = nil, has_symbol = true) + symbol = has_symbol ? '~' : nil + create_token('Label', label_name, symbol) + end + def default_placeholder 'Search or filter results...' end diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index c89389b90ca..ef3c8e7087f 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -1,16 +1,16 @@ #!/usr/bin/env ruby -# +# # # generate-seed-repo-rb -# +# # This script generates the seed_repo.rb file used by lib/gitlab/git # tests. The seed_repo.rb file needs to be updated anytime there is a # Git push to https://gitlab.com/gitlab-org/gitlab-git-test. -# +# # Usage: -# +# # ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb -# -# +# +# require 'erb' require 'tempfile' diff --git a/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc b/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc Binary files differnew file mode 100644 index 00000000000..86bf37ac887 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc diff --git a/spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0 b/spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0 new file mode 100644 index 00000000000..d90cb028e9b --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0 @@ -0,0 +1,2 @@ +xOn1䜯 9&O "noYD6ՒҪ?j;wQ GrN(HPrArR7tpM#McNrsI +%p>۫pz?Y3XBB̰GB4
p?kv۞y~W])[a<CP_
\ No newline at end of file diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs index ce5ab1f705b..507e4ce785a 100644 --- a/spec/support/gitlab-git-test.git/packed-refs +++ b/spec/support/gitlab-git-test.git/packed-refs @@ -8,6 +8,7 @@ 46e1395e609395de004cacd4b142865ab0e52a29 refs/heads/gitattributes-updated 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master 5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test +9596bc54a6f0c0c98248fe97077eb5ccf48a98d0 refs/heads/missing-gitmodules f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0 ^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0 diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index 96ea6f28b30..65b38626a51 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -1,4 +1,6 @@ module GpgHelpers + SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'.freeze + module User1 extend self diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb index a60d3b0d22d..75982432ab4 100644 --- a/spec/support/issuables_list_metadata_shared_examples.rb +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -2,12 +2,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| before do @issuable_ids = [] - 2.times do |n| + %w[fix improve/awesome].each do |source_branch| issuable = if issuable_type == :issue create(issuable_type, project: project) else - create(issuable_type, source_project: project, source_branch: "#{n}-feature") + create(issuable_type, source_project: project, source_branch: source_branch) end @issuable_ids << issuable.id diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb index ff60bd0c0ae..bb6b7c63ee9 100644 --- a/spec/support/matchers/access_matchers_for_controller.rb +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -1,6 +1,6 @@ # AccessMatchersForController # -# For testing authorize_xxx in controller. +# For testing authorize_xxx in controller. module AccessMatchersForController extend RSpec::Matchers::DSL include Warden::Test::Helpers diff --git a/spec/support/matchers/query_matcher.rb b/spec/support/matchers/query_matcher.rb index ac8c4ab91d9..bb0feca7c43 100644 --- a/spec/support/matchers/query_matcher.rb +++ b/spec/support/matchers/query_matcher.rb @@ -28,6 +28,6 @@ RSpec::Matchers.define :make_queries_matching do |matcher, expected_count = nil| def query_count(regex, &block) @recorder = ActiveRecord::QueryRecorder.new(&block).log - @recorder.select{ |q| q.match(regex) } + @recorder.select { |q| q.match(regex) } end end diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb index aabdad13047..255b3d96a62 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/migrations_helpers.rb @@ -31,6 +31,35 @@ module MigrationsHelpers end end + def migration_schema_version + self.class.metadata[:schema] || previous_migration.version + end + + def schema_migrate_down! + disable_migrations_output do + ActiveRecord::Migrator.migrate(migrations_paths, + migration_schema_version) + end + + reset_column_in_migration_models + end + + def schema_migrate_up! + disable_migrations_output do + ActiveRecord::Migrator.migrate(migrations_paths) + end + + reset_column_in_migration_models + end + + def disable_migrations_output + ActiveRecord::Migration.verbose = false + + yield + ensure + ActiveRecord::Migration.verbose = true + end + def migrate! ActiveRecord::Migrator.up(migrations_paths) do |migration| migration.name == described_class.name diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb index 5cb415111d2..86bfeed107c 100644 --- a/spec/support/rake_helpers.rb +++ b/spec/support/rake_helpers.rb @@ -5,11 +5,15 @@ module RakeHelpers end def stub_warn_user_is_not_gitlab - allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab) + allow(main_object).to receive(:warn_user_is_not_gitlab) end def silence_output - allow($stdout).to receive(:puts) - allow($stdout).to receive(:print) + allow(main_object).to receive(:puts) + allow(main_object).to receive(:print) + end + + def main_object + @main_object ||= TOPLEVEL_BINDING.eval('self') end end diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index f9552e41894..8676f895a83 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -3,12 +3,12 @@ RSpec.shared_examples "redis_shared_examples" do let(:test_redis_url) { "redis://redishost:#{redis_port}"} - before(:each) do + before do stub_env(environment_config_file_name, Rails.root.join(config_file_name)) clear_raw_config end - after(:each) do + after do clear_raw_config end diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb index cfe7fc980a8..b4868e82cd7 100644 --- a/spec/support/seed_repo.rb +++ b/spec/support/seed_repo.rb @@ -97,6 +97,7 @@ module SeedRepo gitattributes-updated master merge-test + missing-gitmodules ].freeze TAGS = %w[ v1.0.0 diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 516f8878679..45c10e78789 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -38,6 +38,20 @@ module StubConfiguration allow(Gitlab.config.backup).to receive_messages(to_settings(messages)) end + def stub_storage_settings(messages) + # Default storage is always required + messages['default'] ||= Gitlab.config.repositories.storages.default + messages.each do |storage_name, storage_settings| + storage_settings['path'] ||= TestEnv.repos_path + storage_settings['failure_count_threshold'] ||= 10 + storage_settings['failure_wait_time'] ||= 30 + storage_settings['failure_reset_time'] ||= 1800 + storage_settings['storage_timeout'] ||= 5 + end + + allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages)) + end + private # Modifies stubbed messages to also stub possible predicate versions diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index e059c9620ab..1e39f80699c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -250,6 +250,14 @@ module TestEnv "#{forked_repo_path}_bare" end + def with_empty_bare_repository(name = nil) + path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s + + yield(Rugged::Repository.init_at(path, :bare)) + ensure + FileUtils.rm_rf(path) + end + private def factory_repo_path diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index ff0b47899f5..2dfa5fbecea 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -1,6 +1,6 @@ shared_context 'unique ips sign in limit' do include StubENV - before(:each) do + before do Gitlab::Redis::Cache.with(&:flushall) Gitlab::Redis::Queues.with(&:flushall) Gitlab::Redis::SharedState.with(&:flushall) diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb index ed6c5b09663..83d54259dfa 100644 --- a/spec/tasks/config_lint_spec.rb +++ b/spec/tasks/config_lint_spec.rb @@ -2,19 +2,19 @@ require 'rake_helper' Rake.application.rake_require 'tasks/config_lint' describe ConfigLint do - let(:files){ ['lib/support/fake.sh'] } + let(:files) { ['lib/support/fake.sh'] } it 'errors out if any bash scripts have errors' do - expect { described_class.run(files){ system('exit 1') } }.to raise_error(SystemExit) + expect { described_class.run(files) { system('exit 1') } }.to raise_error(SystemExit) end it 'passes if all scripts are fine' do - expect { described_class.run(files){ system('exit 0') } }.not_to raise_error + expect { described_class.run(files) { system('exit 0') } }.not_to raise_error end end describe 'config_lint rake task' do - before(:each) do + before do # Prevent `system` from actually being called allow(Kernel).to receive(:system).and_return(true) end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index fae92451b46..0c8c8a2ab05 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -119,7 +119,7 @@ describe 'gitlab:app namespace rake task' do let(:project) { create(:project, :repository) } let(:user_backup_path) { "repositories/#{project.disk_path}" } - before(:each) do + before do @origin_cd = Dir.pwd path = File.join(project.repository.path_to_repo, filename) @@ -130,7 +130,7 @@ describe 'gitlab:app namespace rake task' do create_backup end - after(:each) do + after do ENV["SKIP"] = "" FileUtils.rm(@backup_tar) Dir.chdir(@origin_cd) diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index cc932a4ec4c..1e9b20435ec 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -20,7 +20,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do - expect_any_instance_of(Object) + expect(main_object) .to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' @@ -33,7 +33,7 @@ describe 'gitlab:gitaly namespace rake task' do end it 'calls checkout_or_clone_version with the right arguments' do - expect_any_instance_of(Object) + expect(main_object) .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:gitaly:install', clone_path) @@ -41,7 +41,7 @@ describe 'gitlab:gitaly namespace rake task' do end describe 'gmake/make' do - let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT] } + let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] } before(:all) do @old_env_ci = ENV.delete('CI') @@ -54,17 +54,17 @@ describe 'gitlab:gitaly namespace rake task' do before do FileUtils.mkdir_p(clone_path) expect(Dir).to receive(:chdir).with(clone_path).and_call_original + allow(Bundler).to receive(:bundle_path).and_return('/fake/bundle_path') end context 'gmake is available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_version) - allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true) + expect(main_object).to receive(:checkout_or_clone_version) end it 'calls gmake in the gitaly directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) - expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -72,16 +72,27 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is not available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_version) - allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + expect(main_object).to receive(:checkout_or_clone_version) + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) end it 'calls make in the gitaly directory' do - expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) - expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end + + context 'when Rails.env is not "test"' do + before do + allow(Rails.env).to receive(:test?).and_return(false) + end + + it 'calls make in the gitaly directory without BUNDLE_PATH' do + expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + + run_rake_task('gitlab:gitaly:install', clone_path) + end + end end end end diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb index ee3614c50f6..65155cb044d 100644 --- a/spec/tasks/gitlab/shell_rake_spec.rb +++ b/spec/tasks/gitlab/shell_rake_spec.rb @@ -22,7 +22,8 @@ describe 'gitlab:shell rake tasks' do describe 'create_hooks task' do it 'calls gitlab-shell bin/create_hooks' do expect_any_instance_of(Object).to receive(:system) - .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args) + .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", + *Gitlab::TaskHelpers.repository_storage_paths_args) run_rake_task('gitlab:shell:create_hooks') end diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 1b68f3044a4..42516d36c67 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -20,7 +20,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do - expect_any_instance_of(Object) + expect(main_object) .to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error' @@ -33,7 +33,7 @@ describe 'gitlab:workhorse namespace rake task' do end it 'calls checkout_or_clone_version with the right arguments' do - expect_any_instance_of(Object) + expect(main_object) .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:workhorse:install', clone_path) @@ -48,13 +48,13 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_version) - allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) + expect(main_object).to receive(:checkout_or_clone_version) + allow(Object).to receive(:run_command!).with(['gmake']).and_return(true) end it 'calls gmake in the gitlab-workhorse directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) - expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) + expect(main_object).to receive(:run_command!).with(['gmake']).and_return(true) run_rake_task('gitlab:workhorse:install', clone_path) end @@ -62,13 +62,13 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is not available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_version) - allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) + expect(main_object).to receive(:checkout_or_clone_version) + allow(main_object).to receive(:run_command!).with(['make']).and_return(true) end it 'calls make in the gitlab-workhorse directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) - expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) + expect(main_object).to receive(:run_command!).with(['make']).and_return(true) run_rake_task('gitlab:workhorse:install', clone_path) end diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index d7c1b390f9a..0cf462e9553 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -4,11 +4,11 @@ describe FileMover do let(:filename) { 'banana_sample.gif' } let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) } let(:temp_description) do - 'test ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\ - '(/uploads/system/temp/secret55/banana_sample.gif)' + 'test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\ + '(/uploads/-/system/temp/secret55/banana_sample.gif)' end let(:temp_file_path) { File.join('secret55', filename).to_s } - let(:file_path) { File.join('uploads', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } + let(:file_path) { File.join('uploads', '-', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } let(:snippet) { create(:personal_snippet, description: temp_description) } @@ -28,8 +28,8 @@ describe FileMover do expect(snippet.reload.description) .to eq( - "test ![banana_sample](/uploads/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ - " same ![banana_sample](/uploads/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" + "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" ) end @@ -50,8 +50,8 @@ describe FileMover do expect(snippet.reload.description) .to eq( - "test ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif)"\ - " same ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif)" + "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)" ) end diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb index e505edc75ce..cbafa9f478d 100644 --- a/spec/uploaders/personal_file_uploader_spec.rb +++ b/spec/uploaders/personal_file_uploader_spec.rb @@ -10,7 +10,7 @@ describe PersonalFileUploader do dynamic_segment = "personal_snippet/#{snippet.id}" - expect(described_class.absolute_path(upload)).to end_with("/system/#{dynamic_segment}/secret/foo.jpg") + expect(described_class.absolute_path(upload)).to end_with("/-/system/#{dynamic_segment}/secret/foo.jpg") end end @@ -19,7 +19,7 @@ describe PersonalFileUploader do uploader = described_class.new(snippet, 'secret') allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name')) - expected_url = "/uploads/system/personal_snippet/#{snippet.id}/secret/file_name" + expected_url = "/uploads/-/system/personal_snippet/#{snippet.id}/secret/file_name" expect(uploader.to_h).to eq( alt: 'file_name', diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb new file mode 100644 index 00000000000..4c247361bd7 --- /dev/null +++ b/spec/views/projects/commits/_commit.html.haml_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe 'projects/commits/_commit.html.haml' do + context 'with a singed commit' do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:ref) { GpgHelpers::SIGNED_COMMIT_SHA } + let(:commit) { repository.commit(ref) } + + it 'does not display a loading spinner for GPG status' do + render partial: 'projects/commits/commit', locals: { + project: project, + ref: ref, + commit: commit + } + + within '.gpg-status-box' do + expect(page).not_to have_css('i.fa.fa-spinner.fa-spin') + end + end + end +end diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb index 94899e26292..1af422941d7 100644 --- a/spec/views/projects/edit.html.haml_spec.rb +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -11,14 +11,26 @@ describe 'projects/edit' do allow(controller).to receive(:current_user).and_return(user) allow(view).to receive_messages(current_user: user, can?: true) - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end context 'LFS enabled setting' do it 'displays the correct elements' do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + render + expect(rendered).to have_select('project_lfs_enabled') expect(rendered).to have_content('Git Large File Storage') end end + + context 'project export disabled' do + it 'does not display the project export option' do + stub_application_setting(project_export_enabled?: false) + + render + + expect(rendered).not_to have_content('Export project') + end + end end diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb index aea20d826d0..9c0be249a50 100644 --- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb +++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb @@ -24,18 +24,16 @@ describe 'projects/notes/_more_actions_dropdown' do expect(rendered).not_to have_selector('.dropdown.more-actions') end - it 'shows Report as abuse, Edit and Delete buttons if editable and not current users comment' do + it 'shows Report as abuse and Delete buttons if editable and not current users comment' do render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note expect(rendered).to have_link('Report as abuse') - expect(rendered).to have_button('Edit comment') expect(rendered).to have_link('Delete comment') end - it 'shows Edit and Delete buttons if editable and current users comment' do + it 'shows Delete button if editable and current users comment' do render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: true, note: note - expect(rendered).to have_button('Edit comment') expect(rendered).to have_link('Delete comment') end end diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb index c6a17d77d73..54978baca88 100644 --- a/spec/workers/create_gpg_signature_worker_spec.rb +++ b/spec/workers/create_gpg_signature_worker_spec.rb @@ -1,34 +1,26 @@ require 'spec_helper' describe CreateGpgSignatureWorker do + let(:project) { create(:project, :repository) } + context 'when GpgKey is found' do - it 'calls Commit#signature' do - commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' - project = create :project - commit = instance_double(Commit) + let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - allow(Project).to receive(:find_by).with(id: project.id).and_return(project) - allow(project).to receive(:commit).with(commit_sha).and_return(commit) + it 'calls Gitlab::Gpg::Commit#signature' do + expect(Gitlab::Gpg::Commit).to receive(:new).with(project, commit_sha).and_call_original - expect(commit).to receive(:signature) + expect_any_instance_of(Gitlab::Gpg::Commit).to receive(:signature) described_class.new.perform(commit_sha, project.id) end end context 'when Commit is not found' do - let(:nonexisting_commit_sha) { 'bogus' } - let(:project) { create :project } + let(:nonexisting_commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a34' } it 'does not raise errors' do expect { described_class.new.perform(nonexisting_commit_sha, project.id) }.not_to raise_error end - - it 'does not call Commit#signature' do - expect_any_instance_of(Commit).not_to receive(:signature) - - described_class.new.perform(nonexisting_commit_sha, project.id) - end end context 'when Project is not found' do @@ -38,8 +30,8 @@ describe CreateGpgSignatureWorker do expect { described_class.new.perform(anything, nonexisting_project_id) }.not_to raise_error end - it 'does not call Commit#signature' do - expect_any_instance_of(Commit).not_to receive(:signature) + it 'does not call Gitlab::Gpg::Commit#signature' do + expect_any_instance_of(Gitlab::Gpg::Commit).not_to receive(:signature) described_class.new.perform(anything, nonexisting_project_id) end diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb index ed49ce57c0b..4e15ccc534b 100644 --- a/spec/workers/new_issue_worker_spec.rb +++ b/spec/workers/new_issue_worker_spec.rb @@ -41,7 +41,7 @@ describe NewIssueWorker do let(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") } it 'creates a new event record' do - expect{ worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1) + expect { worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1) end it 'creates a notification for the assignee' do diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb index 85af6184d39..9e0cbde45b1 100644 --- a/spec/workers/new_merge_request_worker_spec.rb +++ b/spec/workers/new_merge_request_worker_spec.rb @@ -43,7 +43,7 @@ describe NewMergeRequestWorker do end it 'creates a new event record' do - expect{ worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1) + expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1) end it 'creates a notification for the assignee' do diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index 14ed8b7811e..75197039f5a 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -23,7 +23,7 @@ describe PipelineScheduleWorker do 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) + expect { subject }.to change { project.pipelines.count }.by(1) expect(Ci::Pipeline.last).to be_schedule end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 74a9f90195c..af6a3c9f6c7 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -78,7 +78,7 @@ describe PostReceive do stub_ci_pipeline_to_return_yaml_file end - it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) } + it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } end context "does not create a Ci::Pipeline" do @@ -86,7 +86,7 @@ describe PostReceive do stub_ci_pipeline_yaml_file(nil) end - it { expect{ subject }.not_to change{ Ci::Pipeline.count } } + it { expect { subject }.not_to change { Ci::Pipeline.count } } end end diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb index 35e1518a35e..ea974355050 100644 --- a/spec/workers/prune_old_events_worker_spec.rb +++ b/spec/workers/prune_old_events_worker_spec.rb @@ -2,9 +2,11 @@ require 'spec_helper' describe PruneOldEventsWorker do describe '#perform' do - let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) } - let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) } - let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) } + let(:user) { create(:user) } + + let!(:expired_event) { create(:event, :closed, author: user, created_at: 13.months.ago) } + let!(:not_expired_event) { create(:event, :closed, author: user, created_at: 1.day.ago) } + let!(:exactly_12_months_event) { create(:event, :closed, author: user, created_at: 12.months.ago) } it 'prunes events older than 12 months' do expect { subject.perform }.to change { Event.count }.by(-1) diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index ca904e512ac..100dfc32bbe 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -22,8 +22,8 @@ describe RepositoryImportWorker do it 'hide the credentials that were used in the import URL' do error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + project.update_attributes(import_jid: '123') expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) - allow(subject).to receive(:jid).and_return('123') expect do subject.perform(project.id) diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb index 2f5b685a332..a82eb54ffe4 100644 --- a/spec/workers/stuck_import_jobs_worker_spec.rb +++ b/spec/workers/stuck_import_jobs_worker_spec.rb @@ -8,29 +8,29 @@ describe StuckImportJobsWorker do allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) end - describe 'long running import' do - let(:project) { create(:project, import_jid: '123', import_status: 'started') } + describe 'with started import_status' do + let(:project) { create(:project, :import_started, import_jid: '123') } - before do - allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123']) - end + describe 'long running import' do + it 'marks the project as failed' do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123']) - it 'marks the project as failed' do - expect { worker.perform }.to change { project.reload.import_status }.to('failed') + expect { worker.perform }.to change { project.reload.import_status }.to('failed') + end end - end - describe 'running import' do - let(:project) { create(:project, import_jid: '123', import_status: 'started') } - - before do - allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([]) - end + describe 'running import' do + it 'does not mark the project as failed' do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([]) - it 'does not mark the project as failed' do - worker.perform + expect { worker.perform }.not_to change { project.reload.import_status } + end - expect(project.reload.import_status).to eq('started') + describe 'import without import_jid' do + it 'marks the project as failed' do + expect { worker.perform }.to change { project.reload.import_status }.to('failed') + end + end end end end diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz Binary files differnew file mode 100644 index 00000000000..6353f6605d5 --- /dev/null +++ b/vendor/project_templates/express.tar.gz diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz Binary files differindex b54cae3143a..0509016f130 100644 --- a/vendor/project_templates/rails.tar.gz +++ b/vendor/project_templates/rails.tar.gz diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz Binary files differnew file mode 100644 index 00000000000..d7c0ab74d01 --- /dev/null +++ b/vendor/project_templates/spring.tar.gz diff --git a/yarn.lock b/yarn.lock index c9e1b630a9e..5fc28f8b5ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6275,9 +6275,9 @@ webpack-stats-plugin@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.1.5.tgz#29e5f12ebfd53158d31d656a113ac1f7b86179d9" -webpack@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.0.tgz#e9465b660ad79dd2d33874d968b31746ea9a8e63" +webpack@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.5.4.tgz#5583eb263ed27b78b5bd17bfdfb0eb1b1cd1bf81" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" |