diff options
638 files changed, 6249 insertions, 2796 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 65f2bc7045f..2f149ef9a35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -193,7 +193,7 @@ review-docs-deploy: name: review-docs/$CI_COMMIT_REF_NAME # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 - url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX + url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup script: - ./trigger-build-docs deploy @@ -256,7 +256,7 @@ flaky-examples-check: USE_BUNDLE_INSTALL: "false" NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json stage: post-test - allow_failure: yes + allow_failure: true retry: 0 only: - branches @@ -416,11 +416,10 @@ ee_compat_check: - /^[\d-]+-stable(-ee)?/ - branches@gitlab-org/gitlab-ee - branches@gitlab/gitlab-ee - allow_failure: no retry: 0 artifacts: name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" - when: on_failure + when: always expire_in: 10d paths: - ee_compat_check/patches/*.patch @@ -475,7 +474,7 @@ migration:path-mysql: <<: *pull-cache stage: test script: - - bundle exec rake db:rollback STEP=120 + - bundle exec rake db:rollback STEP=119 - bundle exec rake db:migrate db:rollback-pg: @@ -578,7 +577,7 @@ codequality: 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 + - 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:0.69.0 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: diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md index 1278061a410..5b55eb1374b 100644 --- a/.gitlab/issue_templates/Feature Proposal.md +++ b/.gitlab/issue_templates/Feature Proposal.md @@ -1,22 +1,3 @@ -Please read this! - -Before opening a new issue, make sure to search for keywords in the issues -filtered by the "feature proposal" label: - -For the Community Edition issue tracker: - -- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal - -For the Enterprise Edition issue tracker: - -- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal - -and verify the issue you're about to submit isn't a duplicate. - -Please remove this notice if you're confident your issue isn't a duplicate. - ------- - ### Description (Include problem, use cases, benefits, and/or goals) @@ -25,26 +6,4 @@ Please remove this notice if you're confident your issue isn't a duplicate. ### Links / references -### Documentation blurb - -#### Overview - -What is it? -Why should someone use this feature? -What is the underlying (business) problem? -How do you use this feature? - -#### Use cases - -Who is this for? Provide one or more use cases. - -### Feature checklist - -Make sure these are completed before closing the issue, -with a link to the relevant commit. - -- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance) -- [ ] Documentation -- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml) - -/label ~"feature proposal"
\ No newline at end of file +/label ~"feature proposal" diff --git a/CHANGELOG.md b/CHANGELOG.md index f85b78cb277..48282f67ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,193 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.2.0 (2017-11-22) + +### Security (4 changes) + +- Upgrade Ruby to 2.3.5 to include security patches. !15099 +- Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization. +- Convert private tokens to Personal Access Tokens with sudo scope. +- Remove private tokens from web interface and API. + +### Removed (5 changes) + +- Remove help text from group issues page and group merge requests page. !14963 +- Remove overzealous tooltips in projects page tabs. !15017 +- Stop merge requests from fetching their refs when the data is already available. !15129 +- Remove update merge request worker tagging. +- Remove Session API now that private tokens are removed from user API endpoints. + +### Fixed (75 changes, 18 of them are from the community) + +- Fix 404 errors in API caused when the branch name had a dot. !14462 (gvieira37) +- Remove unnecessary alt-texts from pipeline emails. !14602 (gernberg) +- Renders 404 in commits controller if no commits are found for a given path. !14610 (Guilherme Vieira) +- Cleanup data-page attribute after each Karma test. !14742 +- Removed extra border radius from .file-editor and .file-holder when editing a file. !14803 (Rachel Pipkin) +- Add support for markdown preview to group milestones. !14806 (Vitaliy @blackst0ne Klachkov) +- Fixed 'Removed source branch' checkbox in merge widget being ignored. !14832 +- Fix unnecessary ajax requests in admin broadcast message form. !14853 +- Make NamespaceSelect change URL when filtering. !14888 +- Get true failure from evalulate_script by checking for element beforehand. !14898 +- Fix SAML error 500 when no groups are defined for user. !14913 +- Fix 500 errors caused by empty diffs in some discussions. !14945 (Alexander Popov) +- Fix the atom feed for group events. !14974 +- Hides pipeline duration in commit box when it is zero (nil). !14979 (gvieira37) +- Add new diff discussions on MR diffs tab in "realtime". !14981 +- Returns a ssh url for go-get=1. !14990 (gvieira37) +- Case insensitive search for branches. !14995 (George Andrinopoulos) +- Fixes 404 error to 'Issues assigned to me' and 'Issues I've created' when issues are disabled. !15021 (Jacopo Beschi @jacopo-beschi) +- Update the groups API documentation. !15024 (Robert Schilling) +- Validate username/pw for Jiraservice, require them in the API. !15025 (Robert Schilling) +- Update Merge Request polling so there is only one request at a time. !15032 +- Use project select dropdown not only as a combobutton. !15043 +- Remove create MR button from issues when MRs are disabled. !15071 (George Andrinopoulos) +- Tighten up whitelisting of certain Geo routes. !15082 +- Allow to disable the Performance Bar. !15084 +- Refresh open Issue and Merge Request project counter caches when re-opening. !15085 (Rob Ede @robjtede) +- Fix markdown form tabs toggling preview mode from double clicking write mode button. !15119 +- Fix cancel button not working while uploading on the new issue page. !15137 +- Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex)) +- Fix issues with forked projects of which the source was deleted. !15150 +- Fix GPG signature popup info in Safari and Firefox. !15228 +- Fix GFM reference links for closed milestones. !15234 (Vitaliy @blackst0ne Klachkov) +- When deleting merged branches, ignore protected tags. !15252 +- Revert a regression on runners sorting (!15134). !15341 (Takuya Noguchi) +- Don't use JS to delete memberships from projects and groups. !15344 +- Don't try to create fork network memberships for forks with a missing source. !15366 +- Fix gitlab:backup rake for hashed storage based repositories. !15400 +- Fix issue where clicking a GPG verification badge would scroll to the top of the page. !15407 +- Update container repository path reference and allow using double underscore. !15417 +- Fix crash when navigating to second page of the group dashbaord when there are projects and groups on the first page. !15456 +- Fix flash errors showing up on a non configured prometheus integration. !35652 +- Fix timezone bug in Pikaday and upgrade Pikaday version. +- Fix arguments Import/Export error importing project merge requests. +- Moves mini graph of pipeline to the end of sentence in MR widget. Cleans HTML and tests. +- Fix user autocomplete in subgroups. +- Fixed user profile activity tab being off-screen on mobile. +- Fix diff parser so it tolerates to diff special markers in the content. +- Fix a migration that adds merge_requests_ff_only_enabled column to MR table. +- Don't create build failed todos when the job is automatically retried. +- Render 404 when polling commit notes without having permissions. +- Show error message when fast-forward merge is not possible. +- Prevents position update for image diff notes. +- Mobile-friendly table on Admin Runners. (Takuya Noguchi) +- Decreases z-index of select2 to a lower number of our navigation bar. +- Fix broken Members link when relative URL root paths are used. +- Avoid regenerating the ref path for the environment. +- Memoize GitLab logger to reduce open file descriptors. +- Fix hashed storage with project transfers to another namespace. +- Fix bad type checking to prevent 0 count badge to be shown. +- Fix problem with issuable header wrapping when content is too long. +- Move retry button in job page to sidebar. +- Formats bytes to human reabale number in registry table. +- Fix commit pipeline showing wrong status. +- Include link to issue in reopen message for Slack and Mattermost notifications. +- Fix double border UI bug on pipelines/environments table and pagination. +- Remove native title tooltip in pipeline jobs dropdown in Safari. +- Fix namespacing for MergeWhenPipelineSucceedsService in MR API. +- Prevent error when authorizing an admin-created OAauth application without a set owner. +- Always return full avatar URL for private/internal groups/projects when asset host is set. +- Make sure group and project creation is blocked for new users that are external by default. +- Make sure NotesActions#noteable returns a Noteable in the update action. +- Reallow project paths ending in periods. +- Only set Auto-Submitted header once for emails on push. +- Fix overlap of right-sidebar and main content when creating a Wiki page. +- Enables scroll to bottom once user has scrolled back to bottom in job log. + +### Changed (21 changes, 7 of them are from the community) + +- Added possibility to enter past date in /spend command to log time in the past. !3044 (g3dinua, LockiStrike) +- Add Prometheus equivalent of all InfluxDB metrics. !13891 +- Show collapsible project lists. !14055 +- Make Prometheus metrics endpoint return empty response when metrics are disabled. !14490 +- Support custom attributes on groups and projects. !14593 (Markus Koller) +- Avoid fetching all branches for branch existence checks. !14778 +- Update participants and subscriptions button in issuable sidebar to be async. !14836 +- Replace WikiPage::CreateService calls with wiki_page factory in specs. !14850 (Jacopo Beschi @jacopo-beschi) +- Add lazy option to UserAvatarImage. !14895 +- Add readme only option as project view. !14900 +- Todos spelled correctly on Todos list page. !15015 +- Support uml:: and captions in reStructuredText. !15120 (Markus Koller) +- Add system hooks user_rename and group_rename. !15123 +- Change tags order in refs dropdown. !15235 (Vitaliy @blackst0ne Klachkov) +- Change default cluster size to n1-default-2. !39649 (Fabio Busatto) +- Change 'Sign Out' route from a DELETE to a GET. !39708 (Joe Marty) +- Change background color of nav sidebar to match other gl sidebars. +- Update i18n section in FE docs for marking and interpolation. +- Add a count of changes to the merge requests API. +- Improve GitLab Import rake task to work with Hashed Storage and Subgroups. +- 14830 Move GitLab export option to top of import list when creating a new project. + +### Performance (14 changes) + +- Improve branch listing page performance. !14729 +- Improve DashboardController#activity.json performance. !14985 +- Add a latest_merge_request_diff_id column to merge_requests. !15035 +- Improve performance of the /projects/:id/repository/branches API endpoint. !15215 +- Ensure merge requests with lots of version don't time out when searching for pipelines. +- Speed up issues list APIs. +- Remove Filesystem check metrics that use too much CPU to handle requests. +- Disable Unicorn sampling in Sidekiq since there are no Unicorn sockets to monitor. +- Truncate tree to max 1,000 items and display notice to users. +- Add Performance improvement as category on the changelog. +- Cache commits fetched from the repository. +- Cache the number of user SSH keys. +- Optimise getting the pipeline status of commits. +- Improve performance of commits list by fully using DB index when getting commit note counts. + +### Added (26 changes, 10 of them are from the community) + +- Expose duration in Job entity. !13644 (Mehdi Lahmam (@mehlah)) +- Prevent git push when LFS objects are missing. !13837 +- Automatic configuration settings page. !13850 (Francisco Lopez) +- Add API endpoints for Pages Domains. !13917 (Travis Miller) +- Include the changes in issuable webhook payloads. !14308 +- Add Packagist project service. !14493 (Matt Coleman) +- Add sort runners on admin runners. !14661 (Takuya Noguchi) +- Repo Editor: Add option to start a new MR directly from comit section. !14665 +- Issue JWT token with registry:catalog:* scope when requested by GitLab admin. !14751 (Vratislav Kalenda) +- Support show-all-refs for git over HTTP. !14834 +- Add loading button for new UX paradigm. !14883 +- Get Project Branch API shows an helpful error message on invalid refname. !14884 (Jacopo Beschi @jacopo-beschi) +- Refactor have_http_status into have_gitlab_http_status. !14958 (Jacopo Beschi @jacopo-beschi) +- Suggest to rename the remote for existing repository instructions. !14970 (helmo42) +- Adds project_id to pipeline hook data. !15044 (Jacopo Beschi @jacopo-beschi) +- Hashed Storage support for Attachments. !15068 +- Add metric tagging for sidekiq workers. !15111 +- Expose project visibility as CI variable - CI_PROJECT_VISIBILITY. !15193 +- Allow multiple queries in a single Prometheus graph to support additional environments (Canary, Staging, et al.). !15201 +- Allow promoting project milestones to group milestones. +- Added submodule support in multi-file editor. +- Add applications section to GKE clusters page to easily install Helm Tiller, Ingress. +- Allow files to uploaded in the multi-file editor. +- Add Ingress to available Cluster applications. +- Adds typescript support. +- Add sudo scope for OAuth and Personal Access Tokens to be used by admins to impersonate other users on the API. + +### Other (18 changes, 8 of them are from the community) + +- Decrease Perceived Complexity threshold to 14. !14231 (Maxim Rydkin) +- Replace the 'features/explore/projects.feature' spinach test with an rspec analog. !14755 (Vitaliy @blackst0ne Klachkov) +- While displaying a commit, do not show list of related branches if there are thousands of branches. !14812 +- Removed d3.js from the graph and users bundles and used the common_d3 bundle instead. !14826 +- Make contributors page translatable. !14915 +- Decrease ABC threshold to 54.28. !14920 (Maxim Rydkin) +- Clarify system_hook triggers in documentation. !14957 (Joe Marty) +- Free up some reserved group names. !15052 +- Bump carrierwave to 1.2.1. !15072 (Takuya Noguchi) +- Enable NestingDepth (level 6) on scss-lint. !15073 (Takuya Noguchi) +- Enable BorderZero rule in scss-lint. !15168 (Takuya Noguchi) +- Internationalized tags page. !38589 +- Moves placeholders components into shared folder with documentation. Makes them easier to reuse in MR and Snippets comments. +- Reorganize welcome page for new users. +- Refactor GroupLinksController. (15121) +- Remove filter icon from search bar. +- Use title as placeholder instead of issue title for reusability. +- Add Gitaly metrics to the performance bar. + + ## 10.1.4 (2017-11-14) ### Fixed (4 changes) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4e5fd842df..4930b541ba2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -543,6 +543,7 @@ When having your code reviewed and when reviewing merge requests please take the etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error. +1. The merge request meets the [definition of done](#definition-of-done). ## Definition of done diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7f422a161ae..316ba4bd9e6 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.53.0 +0.55.0 @@ -263,6 +263,8 @@ gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext', '~> 3.2.2', require: false, group: :development +gem 'batch-loader' + # Perf bar gem 'peek', '~> 1.0.1' gem 'peek-gc', '~> 0.0.2' @@ -343,7 +345,7 @@ group :development, :test do gem 'benchmark-ips', '~> 2.3.0', require: false - gem 'license_finder', '~> 2.1.0', require: false + gem 'license_finder', '~> 3.1', require: false gem 'knapsack', '~> 1.11.0' gem 'activerecord_sane_schema_dumper', '0.2' @@ -398,7 +400,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 43555a01037..4787be92365 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) base32 (0.3.2) + batch-loader (1.1.1) bcrypt (3.1.11) bcrypt_pbkdf (1.0.0) benchmark-ips (2.3.0) @@ -83,6 +84,7 @@ GEM bindata (2.4.1) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) + blankslate (2.1.2.4) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) @@ -274,7 +276,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.52.0) + gitaly-proto (0.54.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -354,10 +356,10 @@ GEM rake grape_logging (1.7.0) grape - grpc (1.6.6) + grpc (1.7.2) google-protobuf (~> 3.1) googleapis-common-protos-types (~> 1.0.0) - googleauth (~> 0.5.1) + googleauth (>= 0.5.1, < 0.7) haml (4.0.7) tilt haml_lint (0.26.0) @@ -451,11 +453,13 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) - license_finder (2.1.0) + license_finder (3.1.1) bundler httparty rubyzip thor + toml (= 0.1.2) + with_env (> 1.0) xml-simple licensee (8.7.0) rugged (~> 0.24) @@ -571,6 +575,8 @@ GEM activerecord (>= 4.0, < 5.2) parser (2.4.0.0) ast (~> 2.2) + parslet (1.5.0) + blankslate (~> 2.0) path_expander (1.0.1) peek (1.0.1) concurrent-ruby (>= 0.9.0) @@ -898,6 +904,8 @@ GEM tilt (2.0.6) timecop (0.8.1) timfel-krb5-auth (0.8.3) + toml (0.1.2) + parslet (~> 1.5.0) toml-rb (0.3.15) citrus (~> 3.0, > 3.0) truncato (0.7.10) @@ -952,6 +960,7 @@ GEM builder expression_parser rinku + with_env (1.1.0) xml-simple (1.1.5) xpath (2.1.0) nokogiri (~> 1.3) @@ -974,6 +983,7 @@ DEPENDENCIES awesome_print (~> 1.2.0) babosa (~> 1.0.2) base32 (~> 0.3.0) + batch-loader bcrypt_pbkdf (~> 1.0) benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) @@ -1026,7 +1036,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.52.0) + gitaly-proto (~> 0.54.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1058,7 +1068,7 @@ DEPENDENCIES knapsack (~> 1.11.0) kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) - license_finder (~> 2.1.0) + license_finder (~> 3.1) licensee (~> 8.7.0) lograge (~> 0.5) loofah (~> 2.0.3) @@ -1 +1 @@ -10.2.0-pre +10.3.0-pre diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index b5500ac116f..6b06344f5ba 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -1,7 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ /* global EditBlob */ -/* global NewCommitForm */ - +import NewCommitForm from '../new_commit_form'; import EditBlob from './edit_blob'; import BlobFileDropzone from '../blob/blob_file_dropzone'; diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index ef4093b59e3..20d23162940 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,12 +1,13 @@ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ -/* global BoardService */ import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; import Flash from '../flash'; +import { __ } from '../locale'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; +import sidebarEventHub from '../sidebar/event_hub'; import './models/issue'; import './models/label'; import './models/list'; @@ -14,7 +15,7 @@ import './models/milestone'; import './models/assignee'; import './stores/boards_store'; import './stores/modal_store'; -import './services/board_service'; +import BoardService from './services/board_service'; import './mixins/modal_mixins'; import './mixins/sortable_default_options'; import './filters/due_date_filters'; @@ -77,11 +78,16 @@ $(() => { }); Store.rootPath = this.boardsEndpoint; - // Listen for updateTokens event eventHub.$on('updateTokens', this.updateTokens); + eventHub.$on('newDetailIssue', this.updateDetailIssue); + eventHub.$on('clearDetailIssue', this.clearDetailIssue); + sidebarEventHub.$on('toggleSubscription', this.toggleSubscription); }, beforeDestroy() { eventHub.$off('updateTokens', this.updateTokens); + eventHub.$off('newDetailIssue', this.updateDetailIssue); + eventHub.$off('clearDetailIssue', this.clearDetailIssue); + sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); }, mounted () { this.filterManager = new FilteredSearchBoards(Store.filter, true); @@ -112,6 +118,46 @@ $(() => { methods: { updateTokens() { this.filterManager.updateTokens(); + }, + updateDetailIssue(newIssue) { + const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint; + if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { + newIssue.setFetchingState('subscriptions', true); + BoardService.getIssueInfo(sidebarInfoEndpoint) + .then(res => res.json()) + .then((data) => { + newIssue.setFetchingState('subscriptions', false); + newIssue.updateData({ + subscribed: data.subscribed, + }); + }) + .catch(() => { + newIssue.setFetchingState('subscriptions', false); + Flash(__('An error occurred while fetching sidebar data')); + }); + } + + Store.detail.issue = newIssue; + }, + clearDetailIssue() { + Store.detail.issue = {}; + }, + toggleSubscription(id) { + const issue = Store.detail.issue; + if (issue.id === id && issue.toggleSubscriptionEndpoint) { + issue.setFetchingState('subscriptions', true); + BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint) + .then(() => { + issue.setFetchingState('subscriptions', false); + issue.updateData({ + subscribed: !issue.subscribed, + }); + }) + .catch(() => { + issue.setFetchingState('subscriptions', false); + Flash(__('An error occurred when toggling the notification subscription')); + }); + } } }, }); diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.vue index 079fb6438b9..0b220a56e0b 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,25 +1,11 @@ +<script> import './issue_card_inner'; +import eventHub from '../eventhub'; const Store = gl.issueBoards.BoardsStore; export default { name: 'BoardsIssueCard', - template: ` - <li class="card" - :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" - :index="index" - :data-issue-id="issue.id" - @mousedown="mouseDown" - @mousemove="mouseMove" - @mouseup="showIssue($event)"> - <issue-card-inner - :list="list" - :issue="issue" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :update-filters="true" /> - </li> - `, components: { 'issue-card-inner': gl.issueBoards.IssueCardInner, }, @@ -56,12 +42,30 @@ export default { this.showDetail = false; if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { - Store.detail.issue = {}; + eventHub.$emit('clearDetailIssue'); } else { - Store.detail.issue = this.issue; + eventHub.$emit('newDetailIssue', this.issue); Store.detail.list = this.list; } } }, }, }; +</script> + +<template> + <li class="card" + :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" + :index="index" + :data-issue-id="issue.id" + @mousedown="mouseDown" + @mousemove="mouseMove" + @mouseup="showIssue($event)"> + <issue-card-inner + :list="list" + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :update-filters="true" /> + </li> +</template> diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 6159680f1e6..29aeb8e84aa 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,6 +1,6 @@ /* global Sortable */ import boardNewIssue from './board_new_issue'; -import boardCard from './board_card'; +import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 9ae5e270a4b..faa76da964f 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -5,12 +5,13 @@ import Vue from 'vue'; import Flash from '../../flash'; import eventHub from '../../sidebar/event_hub'; -import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; -import Assignees from '../../sidebar/components/assignees/assignees'; +import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; +import assignees from '../../sidebar/components/assignees/assignees'; import DueDateSelectors from '../../due_date_select'; import './sidebar/remove_issue'; import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; +import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; const Store = gl.issueBoards.BoardsStore; @@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({ new DueDateSelectors(); new LabelsSelect(); new Sidebar(); - gl.Subscription.bindAll('.subscription'); }, components: { + assigneeTitle, + assignees, removeBtn: gl.issueBoards.RemoveIssueBtn, - 'assignee-title': AssigneeTitle, - assignees: Assignees, + subscriptions, }, }); diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 407db176446..10f85c1d676 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -17,6 +17,11 @@ class ListIssue { this.assignees = []; this.selected = false; this.position = obj.relative_position || Infinity; + this.isFetching = { + subscriptions: true, + }; + this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; + this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; if (obj.milestone) { this.milestone = new ListMilestone(obj.milestone); @@ -73,6 +78,14 @@ class ListIssue { return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); } + updateData(newData) { + Object.assign(this, newData); + } + + setFetchingState(key, value) { + this.isFetching[key] = value; + } + update (url) { const data = { issue: { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 97e80afa3f8..fa7ddd25e1f 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -2,7 +2,7 @@ import Vue from 'vue'; -class BoardService { +export default class BoardService { constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { issues: { @@ -88,6 +88,14 @@ class BoardService { return this.issues.bulkUpdate(data); } + + static getIssueInfo(endpoint) { + return Vue.http.get(endpoint); + } + + static toggleIssueSubscription(endpoint) { + return Vue.http.post(endpoint); + } } window.BoardService = BoardService; diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 0ac8e68187d..ce14c9a9945 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -1,10 +1,7 @@ -import axios from 'axios'; -import setAxiosCsrfToken from '../../lib/utils/axios_utils'; +import axios from '../../lib/utils/axios_utils'; export default class ClusterService { constructor(options = {}) { - setAxiosCsrfToken(); - this.options = options; this.appInstallEndpointMap = { helm: this.options.installHelmEndpoint, @@ -18,7 +15,6 @@ export default class ClusterService { } installApplication(appId) { - const endpoint = this.appInstallEndpointMap[appId]; - return axios.post(endpoint); + return axios.post(this.appInstallEndpointMap[appId]); } } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d716218d9a4..34708977d20 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,12 +1,12 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ import { s__ } from './locale'; -/* global ProjectSelect */ +import projectSelect from './project_select'; import IssuableIndex from './issuable_index'; -/* global Milestone */ +import Milestone from './milestone'; import IssuableForm from './issuable_form'; import LabelsSelect from './labels_select'; /* global MilestoneSelect */ -/* global NewBranchForm */ +import NewBranchForm from './new_branch_form'; /* global NotificationsForm */ /* global NotificationsDropdown */ import groupAvatar from './group_avatar'; @@ -18,16 +18,14 @@ import groupsSelect from './groups_select'; /* global Search */ /* global Admin */ import NamespaceSelect from './namespace_select'; -/* global NewCommitForm */ -/* global NewBranchForm */ +import NewCommitForm from './new_commit_form'; import Project from './project'; import projectAvatar from './project_avatar'; /* global MergeRequest */ /* global Compare */ /* global CompareAutocomplete */ /* global ProjectFindFile */ -/* global ProjectNew */ -/* global ProjectShow */ +import ProjectNew from './project_new'; import projectImport from './project_import'; import Labels from './labels'; import LabelManager from './label_manager'; @@ -91,6 +89,8 @@ import Members from './members'; import memberExpirationDate from './member_expiration_date'; import DueDateSelectors from './due_date_select'; import Diff from './diff'; +import ProjectLabelSubscription from './project_label_subscription'; +import ProjectVariables from './project_variables'; (function() { var Dispatcher; @@ -187,7 +187,7 @@ import Diff from './diff'; initIssuableSidebar(); break; case 'dashboard:milestones:index': - new ProjectSelect(); + projectSelect(); break; case 'projects:milestones:show': case 'groups:milestones:show': @@ -197,7 +197,7 @@ import Diff from './diff'; break; case 'dashboard:issues': case 'dashboard:merge_requests': - new ProjectSelect(); + projectSelect(); initLegacyFilters(); break; case 'groups:issues': @@ -206,7 +206,7 @@ import Diff from './diff'; const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } - new ProjectSelect(); + projectSelect(); break; case 'dashboard:todos:index': new Todos(); @@ -317,7 +317,6 @@ import Diff from './diff'; break; case 'projects:merge_requests:show': new Diff(); - shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); initIssuableSidebar(); @@ -327,6 +326,8 @@ import Diff from './diff'; window.mergeRequest = new MergeRequest({ action: mrShowNode.dataset.mrAction, }); + + shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': new gl.Activities(); @@ -339,7 +340,8 @@ import Diff from './diff'; container: '.js-commit-pipeline-graph', }).bindEvents(); initNotes(); - initChangesDropdown(); + const stickyBarPaddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:commit:pipelines': @@ -484,7 +486,7 @@ import Diff from './diff'; if ($el.find('.dropdown-group-label').length) { new GroupLabelSubscription($el); } else { - new gl.ProjectLabelSubscription($el); + new ProjectLabelSubscription($el); } }); break; @@ -520,7 +522,7 @@ import Diff from './diff'; // Initialize expandable settings panels initSettingsPanels(); case 'groups:settings:ci_cd:show': - new gl.ProjectVariables(); + new ProjectVariables(); break; case 'ci:lints:create': case 'ci:lints:show': @@ -623,7 +625,6 @@ import Diff from './diff'; case 'show': new Star(); new ProjectNew(); - new ProjectShow(); new NotificationsDropdown(); break; case 'wikis': diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index c039ae85cfb..ffb7757bed8 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -227,25 +227,27 @@ export default { /> <div - class="blank-state blank-state-no-icon" + class="blank-state-row" v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any environments right now. - </h2> - <p class="blank-state-text"> - Environments are places where code gets deployed, such as staging or production. - <br /> - <a :href="helpPagePath"> - Read more about environments + <div class="blank-state-center"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments + </a> + </p> + + <a + v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New environment </a> - </p> - - <a - v-if="canCreateEnvironmentParsed" - :href="newEnvironmentPath" - class="btn btn-create js-new-environment-button"> - New environment - </a> + </div> </div> <div diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 1191e0b895e..ada693afc46 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -14,7 +14,6 @@ export default () => { }); new LabelsSelect(); new IssuableContext(sidebarOptions.currentUser); - gl.Subscription.bindAll('.subscription'); new DueDateSelectors(); window.sidebar = new Sidebar(); }; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js index 1b265721581..2cbb70220d0 100644 --- a/app/assets/javascripts/init_legacy_filters.js +++ b/app/assets/javascripts/init_legacy_filters.js @@ -1,8 +1,7 @@ /* eslint-disable no-new */ import LabelsSelect from './labels_select'; /* global MilestoneSelect */ -/* global SubscriptionSelect */ - +import subscriptionSelect from './subscription_select'; import UsersSelect from './users_select'; import issueStatusSelect from './issue_status_select'; @@ -11,5 +10,5 @@ export default () => { new LabelsSelect(); new MilestoneSelect(); issueStatusSelect(); - new SubscriptionSelect(); + subscriptionSelect(); }; diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index af6358953cf..ba2b6737988 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,11 +1,10 @@ /* eslint-disable class-methods-use-this, no-new */ /* global MilestoneSelect */ -/* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import './milestone_select'; import issueStatusSelect from './issue_status_select'; -import './subscription_select'; +import subscriptionSelect from './subscription_select'; import LabelsSelect from './labels_select'; const HIDDEN_CLASS = 'hidden'; @@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar { new LabelsSelect(); new MilestoneSelect(); issueStatusSelect(); - new SubscriptionSelect(); + subscriptionSelect(); } setupBulkUpdateActions() { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index d1aa83ea57f..4e39d483b31 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -29,6 +29,11 @@ export default { required: false, default: false, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, issuableRef: { type: String, required: true, @@ -92,6 +97,16 @@ export default { type: String, required: true, }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, data() { const store = new Store({ @@ -157,21 +172,21 @@ export default { }) .catch(() => { eventHub.$emit('close.form'); - window.Flash('Error updating issue'); + window.Flash(`Error updating ${this.issuableType}`); }); }, deleteIssuable() { this.service.deleteIssuable() .then(res => res.json()) .then((data) => { - // Stop the poll so we don't get 404's with the issue not existing + // Stop the poll so we don't get 404's with the issuable not existing this.poll.stop(); gl.utils.visitUrl(data.web_url); }) .catch(() => { eventHub.$emit('close.form'); - window.Flash('Error deleting issue'); + window.Flash(`Error deleting ${this.issuableType}`); }); }, }, @@ -223,6 +238,8 @@ export default { :markdown-preview-path="markdownPreviewPath" :project-path="projectPath" :project-namespace="projectNamespace" + :show-delete-button="showDeleteButton" + :can-attach-file="canAttachFile" /> <div v-else> <title-component diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 8c81575fe6f..a539506bce2 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -13,6 +13,11 @@ type: Object, required: true, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -23,6 +28,9 @@ isSubmitEnabled() { return this.formState.title.trim() !== ''; }, + shouldShowDeleteButton() { + return this.canDestroy && this.showDeleteButton; + }, }, methods: { closeForm() { @@ -62,7 +70,7 @@ Cancel </button> <button - v-if="canDestroy" + v-if="shouldShowDeleteButton" class="btn btn-danger pull-right append-right-default" :class="{ disabled: deleteLoading }" type="button" diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 0aa1b2c2e31..4d2ef409bad 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -17,6 +17,11 @@ type: String, required: true, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, components: { markdownField, @@ -36,7 +41,8 @@ </label> <markdown-field :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath"> + :markdown-docs-path="markdownDocsPath" + :can-attach-file="canAttachFile"> <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 28bf6c67ea5..d61776d480d 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -36,6 +36,16 @@ type: String, required: true, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, components: { lockedWarning, @@ -78,9 +88,11 @@ <description-field :form-state="formState" :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" /> + :markdown-docs-path="markdownDocsPath" + :can-attach-file="canAttachFile" /> <edit-actions :form-state="formState" - :can-destroy="canDestroy" /> + :can-destroy="canDestroy" + :show-delete-button="showDeleteButton" /> </form> </template> diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index 3e2658f9fc1..5a216f8fae2 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -29,8 +29,8 @@ export default class JobMediator { this.poll = new Poll({ resource: this.service, method: 'getJob', - successCallback: this.successCallback.bind(this), - errorCallback: this.errorCallback.bind(this), + successCallback: response => this.successCallback(response), + errorCallback: () => this.errorCallback(), }); if (!Visibility.hidden()) { @@ -57,7 +57,7 @@ export default class JobMediator { successCallback(response) { this.state.isLoading = false; - return response.json().then(data => this.store.storeJob(data)); + return this.store.storeJob(response.data); } errorCallback() { diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js index eaf1c6e500a..b746489c45c 100644 --- a/app/assets/javascripts/jobs/services/job_service.js +++ b/app/assets/javascripts/jobs/services/job_service.js @@ -1,14 +1,11 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '../../lib/utils/axios_utils'; export default class JobService { constructor(endpoint) { - this.job = Vue.resource(endpoint); + this.job = endpoint; } getJob() { - return this.job.get(); + return axios.get(this.job); } } diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 45bff245827..7aeeca3b283 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -1,6 +1,22 @@ import axios from 'axios'; import csrf from './csrf'; -export default function setAxiosCsrfToken() { - axios.defaults.headers.common[csrf.headerKey] = csrf.token; -} +axios.defaults.headers.common[csrf.headerKey] = csrf.token; + +// Maintain a global counter for active requests +// see: spec/support/wait_for_requests.rb +axios.interceptors.request.use((config) => { + window.activeVueResources = window.activeVueResources || 0; + window.activeVueResources += 1; + + return config; +}); + +// Remove the global counter +axios.interceptors.response.use((config) => { + window.activeVueResources -= 1; + + return config; +}); + +export default axios; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 65a8cf2c891..7fca80c2fdb 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils'; /** * Polling utility for handling realtime updates. - * Service for vue resouce and method need to be provided as props + * Requirements: Promise based HTTP client + * + * Service for promise based http client and method need to be provided as props * * @example * new Poll({ diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0035dd23011..d908452399c 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -29,7 +29,6 @@ import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; -import './lib/utils/pretty_time'; import './lib/utils/url_utility'; // behaviors @@ -59,11 +58,7 @@ import './line_highlighter'; import initLogoAnimation from './logo'; import './merge_request'; import './merge_request_tabs'; -import './milestone'; import './milestone_select'; -import './namespace_select'; -import './new_branch_form'; -import './new_commit_form'; import './notes'; import './notifications_dropdown'; import './notifications_form'; @@ -71,22 +66,15 @@ import './pager'; import './preview_markdown'; import './project_find_file'; import './project_import'; -import './project_label_subscription'; -import './project_new'; -import './project_select'; -import './project_show'; -import './project_variables'; import './projects_dropdown'; import './projects_list'; import './syntax_highlight'; import './render_math'; +import './render_mermaid'; import './render_gfm'; import './right_sidebar'; import './search'; import './search_autocomplete'; -import './smart_interval'; -import './subscription'; -import './subscription_select'; import initBreadcrumbs from './breadcrumb'; import './dispatcher'; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 8f3f1986763..f76a998bf8c 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,54 +1,49 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */ /* global Sortable */ import Flash from './flash'; -(function() { - this.Milestone = (function() { - function Milestone() { - this.bindTabsSwitching(); +export default class Milestone { + constructor() { + this.bindTabsSwitching(); - // Load merge request tab if it is active - // merge request tab is active based on different conditions in the backend - this.loadTab($('.js-milestone-tabs .active a')); + // Load merge request tab if it is active + // merge request tab is active based on different conditions in the backend + this.loadTab($('.js-milestone-tabs .active a')); - this.loadInitialTab(); - } + this.loadInitialTab(); + } + + bindTabsSwitching() { + return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { + const $target = $(e.target); - Milestone.prototype.bindTabsSwitching = function() { - return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { - const $target = $(e.target); + location.hash = $target.attr('href'); + this.loadTab($target); + }); + } + // eslint-disable-next-line class-methods-use-this + loadInitialTab() { + const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); - location.hash = $target.attr('href'); - this.loadTab($target); + if ($target.length) { + $target.tab('show'); + } + } + // eslint-disable-next-line class-methods-use-this + loadTab($target) { + const endpoint = $target.data('endpoint'); + const tabElId = $target.attr('href'); + + if (endpoint && !$target.hasClass('is-loaded')) { + $.ajax({ + url: endpoint, + dataType: 'JSON', + }) + .fail(() => new Flash('Error loading milestone tab')) + .done((data) => { + $(tabElId).html(data.html); + $target.addClass('is-loaded'); }); - }; - - Milestone.prototype.loadInitialTab = function() { - const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); - - if ($target.length) { - $target.tab('show'); - } - }; - - Milestone.prototype.loadTab = function($target) { - const endpoint = $target.data('endpoint'); - const tabElId = $target.attr('href'); - - if (endpoint && !$target.hasClass('is-loaded')) { - $.ajax({ - url: endpoint, - dataType: 'JSON', - }) - .fail(() => new Flash('Error loading milestone tab')) - .done((data) => { - $(tabElId).html(data.html); - $target.addClass('is-loaded'); - }); - } - }; - - return Milestone; - })(); -}).call(window); + } + } +} diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 39fb302b644..77733b67c4d 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,97 +1,93 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ -import RefSelectDropdown from '~/ref_select_dropdown'; +import RefSelectDropdown from './ref_select_dropdown'; -(function() { - this.NewBranchForm = (function() { - function NewBranchForm(form, availableRefs) { - this.validate = this.validate.bind(this); - this.branchNameError = form.find('.js-branch-name-error'); - this.name = form.find('.js-branch-name'); - this.ref = form.find('#ref'); - new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new - this.setupRestrictions(); - this.addBinding(); - this.init(); +export default class NewBranchForm { + constructor(form, availableRefs) { + this.validate = this.validate.bind(this); + this.branchNameError = form.find('.js-branch-name-error'); + this.name = form.find('.js-branch-name'); + this.ref = form.find('#ref'); + new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new + this.setupRestrictions(); + this.addBinding(); + this.init(); + } + + addBinding() { + return this.name.on('blur', this.validate); + } + + init() { + if (this.name.length && this.name.val().length > 0) { + return this.name.trigger('blur'); } + } - NewBranchForm.prototype.addBinding = function() { - return this.name.on('blur', this.validate); + setupRestrictions() { + var endsWith, invalid, single, startsWith; + startsWith = { + pattern: /^(\/|\.)/g, + prefix: "can't start with", + conjunction: "or" }; - - NewBranchForm.prototype.init = function() { - if (this.name.length && this.name.val().length > 0) { - return this.name.trigger('blur'); - } + endsWith = { + pattern: /(\/|\.|\.lock)$/g, + prefix: "can't end in", + conjunction: "or" }; - - NewBranchForm.prototype.setupRestrictions = function() { - var endsWith, invalid, single, startsWith; - startsWith = { - pattern: /^(\/|\.)/g, - prefix: "can't start with", - conjunction: "or" - }; - endsWith = { - pattern: /(\/|\.|\.lock)$/g, - prefix: "can't end in", - conjunction: "or" - }; - invalid = { - pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, - prefix: "can't contain", - conjunction: ", " - }; - single = { - pattern: /^@+$/g, - prefix: "can't be", - conjunction: "or" - }; - return this.restrictions = [startsWith, invalid, endsWith, single]; + invalid = { + pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, + prefix: "can't contain", + conjunction: ", " + }; + single = { + pattern: /^@+$/g, + prefix: "can't be", + conjunction: "or" }; + return this.restrictions = [startsWith, invalid, endsWith, single]; + } - NewBranchForm.prototype.validate = function() { - var errorMessage, errors, formatter, unique, validator; - const indexOf = [].indexOf; + validate() { + var errorMessage, errors, formatter, unique, validator; + const indexOf = [].indexOf; - this.branchNameError.empty(); - unique = function(values, value) { - if (indexOf.call(values, value) === -1) { - values.push(value); - } - return values; - }; - formatter = function(values, restriction) { - var formatted; - formatted = values.map(function(value) { - switch (false) { - case !/\s/.test(value): - return 'spaces'; - case !/\/{2,}/g.test(value): - return 'consecutive slashes'; - default: - return "'" + value + "'"; - } - }); - return restriction.prefix + " " + (formatted.join(restriction.conjunction)); - }; - validator = (function(_this) { - return function(errors, restriction) { - var matched; - matched = _this.name.val().match(restriction.pattern); - if (matched) { - return errors.concat(formatter(matched.reduce(unique, []), restriction)); - } else { - return errors; - } - }; - })(this); - errors = this.restrictions.reduce(validator, []); - if (errors.length > 0) { - errorMessage = $("<span/>").text(errors.join(', ')); - return this.branchNameError.append(errorMessage); + this.branchNameError.empty(); + unique = function(values, value) { + if (indexOf.call(values, value) === -1) { + values.push(value); } + return values; }; - - return NewBranchForm; - })(); -}).call(window); + formatter = function(values, restriction) { + var formatted; + formatted = values.map(function(value) { + switch (false) { + case !/\s/.test(value): + return 'spaces'; + case !/\/{2,}/g.test(value): + return 'consecutive slashes'; + default: + return "'" + value + "'"; + } + }); + return restriction.prefix + " " + (formatted.join(restriction.conjunction)); + }; + validator = (function(_this) { + return function(errors, restriction) { + var matched; + matched = _this.name.val().match(restriction.pattern); + if (matched) { + return errors.concat(formatter(matched.reduce(unique, []), restriction)); + } else { + return errors; + } + }; + })(this); + errors = this.restrictions.reduce(validator, []); + if (errors.length > 0) { + errorMessage = $("<span/>").text(errors.join(', ')); + return this.branchNameError.append(errorMessage); + } + } +} diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 04073ef7270..6e152497d20 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -1,32 +1,28 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */ -(function() { - this.NewCommitForm = (function() { - function NewCommitForm(form) { - this.form = form; - this.renderDestination = this.renderDestination.bind(this); - this.branchName = form.find('.js-branch-name'); - this.originalBranch = form.find('.js-original-branch'); - this.createMergeRequest = form.find('.js-create-merge-request'); - this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); - this.branchName.keyup(this.renderDestination); - this.renderDestination(); - } +export default class NewCommitForm { + constructor(form) { + this.form = form; + this.renderDestination = this.renderDestination.bind(this); + this.branchName = form.find('.js-branch-name'); + this.originalBranch = form.find('.js-original-branch'); + this.createMergeRequest = form.find('.js-create-merge-request'); + this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); + this.branchName.keyup(this.renderDestination); + this.renderDestination(); + } - NewCommitForm.prototype.renderDestination = function() { - var different; - different = this.branchName.val() !== this.originalBranch.val(); - if (different) { - this.createMergeRequestContainer.show(); - if (!this.wasDifferent) { - this.createMergeRequest.prop('checked', true); - } - } else { - this.createMergeRequestContainer.hide(); - this.createMergeRequest.prop('checked', false); + renderDestination() { + var different; + different = this.branchName.val() !== this.originalBranch.val(); + if (different) { + this.createMergeRequestContainer.show(); + if (!this.wasDifferent) { + this.createMergeRequest.prop('checked', true); } - return this.wasDifferent = different; - }; - - return NewCommitForm; - })(); -}).call(window); + } else { + this.createMergeRequestContainer.hide(); + this.createMergeRequest.prop('checked', false); + } + return this.wasDifferent = different; + } +} diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue index e73ec2aaf71..64466b04b40 100644 --- a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue @@ -1,18 +1,21 @@ <script> + import Icon from '../../vue_shared/components/icon.vue'; + export default { - computed: { - lockIcon() { - return gl.utils.spriteIcon('lock'); - }, + component: { + Icon, }, }; - </script> <template> <div class="disabled-comment text-center"> - <span class="issuable-note-warning"> - <span class="icon" v-html="lockIcon"></span> + <span class="issuable-note-warning inline"> + <icon + name="lock" + :size="16" + class="icon"> + </icon> <span>This issue is locked. Only <b>project members</b> can comment.</span> </span> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index cf241c8ffed..233be8a49c8 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -267,9 +267,11 @@ /> <div - class="blank-state blank-state-no-icon" + class="blank-state-row" v-if="shouldRenderNoPipelinesMessage"> - <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + <div class="blank-state-center"> + <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + </div> </div> <div diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index ddb78aaeea1..36b6a5ed376 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ -/* global ProjectSelect */ import Cookies from 'js-cookie'; +import projectSelect from './project_select'; export default class Project { constructor() { @@ -46,7 +46,7 @@ export default class Project { } static projectSelectDropdown () { - new ProjectSelect(); + projectSelect(); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); } diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 0a811627600..b65521b278f 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -1,55 +1,50 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */ +export default class ProjectLabelSubscription { + constructor(container) { + this.$container = $(container); + this.$buttons = this.$container.find('.js-subscribe-button'); -(function(global) { - class ProjectLabelSubscription { - constructor(container) { - this.$container = $(container); - this.$buttons = this.$container.find('.js-subscribe-button'); - - this.$buttons.on('click', this.toggleSubscription.bind(this)); - } + this.$buttons.on('click', this.toggleSubscription.bind(this)); + } - toggleSubscription(event) { - event.preventDefault(); + toggleSubscription(event) { + event.preventDefault(); - const $btn = $(event.currentTarget); - const $span = $btn.find('span'); - const url = $btn.attr('data-url'); - const oldStatus = $btn.attr('data-status'); + const $btn = $(event.currentTarget); + const $span = $btn.find('span'); + const url = $btn.attr('data-url'); + const oldStatus = $btn.attr('data-status'); - $btn.addClass('disabled'); - $span.toggleClass('hidden'); + $btn.addClass('disabled'); + $span.toggleClass('hidden'); - $.ajax({ - type: 'POST', - url: url - }).done(() => { - let newStatus, newAction; + $.ajax({ + type: 'POST', + url, + }).done(() => { + let newStatus; + let newAction; - if (oldStatus === 'unsubscribed') { - [newStatus, newAction] = ['subscribed', 'Unsubscribe']; - } else { - [newStatus, newAction] = ['unsubscribed', 'Subscribe']; - } + if (oldStatus === 'unsubscribed') { + [newStatus, newAction] = ['subscribed', 'Unsubscribe']; + } else { + [newStatus, newAction] = ['unsubscribed', 'Subscribe']; + } - $span.toggleClass('hidden'); - $btn.removeClass('disabled'); + $span.toggleClass('hidden'); + $btn.removeClass('disabled'); - this.$buttons.attr('data-status', newStatus); - this.$buttons.find('> span').text(newAction); + this.$buttons.attr('data-status', newStatus); + this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { - const $button = $(button); + this.$buttons.map((button) => { + const $button = $(button); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); - } + if ($button.attr('data-original-title')) { + $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); + } - return button; - }); + return button; }); - } + }); } - - global.ProjectLabelSubscription = ProjectLabelSubscription; -})(window.gl || (window.gl = {})); +} diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index fd89a1a85c3..ca548d011b6 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ import VisibilitySelect from './visibility_select'; @@ -7,153 +7,145 @@ function highlightChanges($elm) { setTimeout(() => $elm.removeClass('highlight-changes'), 10); } -(function() { - this.ProjectNew = (function() { - function ProjectNew() { - this.toggleSettings = this.toggleSettings.bind(this); - this.$selects = $('.features select'); - this.$repoSelects = this.$selects.filter('.js-repo-select'); - this.$projectSelects = this.$selects.not('.js-repo-select'); - - $('.project-edit-container').on('ajax:before', (function(_this) { - return function() { - $('.project-edit-container').hide(); - return $('.save-project-loader').show(); - }; - })(this)); - - this.initVisibilitySelect(); - - this.toggleSettings(); - this.toggleSettingsOnclick(); - this.toggleRepoVisibility(); - } - - ProjectNew.prototype.initVisibilitySelect = function() { - const visibilityContainer = document.querySelector('.js-visibility-select'); - if (!visibilityContainer) return; - const visibilitySelect = new VisibilitySelect(visibilityContainer); - visibilitySelect.init(); - - const $visibilitySelect = $(visibilityContainer).find('select'); - let projectVisibility = $visibilitySelect.val(); - const PROJECT_VISIBILITY_PRIVATE = '0'; - - $visibilitySelect.on('change', () => { - const newProjectVisibility = $visibilitySelect.val(); - - if (projectVisibility !== newProjectVisibility) { - this.$projectSelects.each((idx, select) => { - const $select = $(select); - const $options = $select.find('option'); - const values = $.map($options, e => e.value); - - // if switched to "private", limit visibility options - if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { - if ($select.val() !== values[0] && $select.val() !== values[1]) { - $select.val(values[1]).trigger('change'); - highlightChanges($select); - } - $options.slice(2).disable(); +export default class ProjectNew { + constructor() { + this.toggleSettings = this.toggleSettings.bind(this); + this.$selects = $('.features select'); + this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); + + $('.project-edit-container').on('ajax:before', () => { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }); + + this.initVisibilitySelect(); + + this.toggleSettings(); + this.toggleSettingsOnclick(); + this.toggleRepoVisibility(); + } + + initVisibilitySelect() { + const visibilityContainer = document.querySelector('.js-visibility-select'); + if (!visibilityContainer) return; + const visibilitySelect = new VisibilitySelect(visibilityContainer); + visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); } + $options.slice(2).disable(); + } - // if switched from "private", increase visibility for non-disabled options - if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { - $options.enable(); - if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { - $select.val(values[values.length - 1]).trigger('change'); - highlightChanges($select); - } + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); } - }); + } + }); - projectVisibility = newProjectVisibility; - } - }); - }; - - ProjectNew.prototype.toggleSettings = function() { - var self = this; - - this.$selects.each(function () { - var $select = $(this); - var className = $select.data('field') - .replace(/_/g, '-') - .replace('access-level', 'feature'); - self._showOrHide($select, '.' + className); - }); - }; - - ProjectNew.prototype.toggleSettingsOnclick = function() { - this.$selects.on('change', this.toggleSettings); - }; - - ProjectNew.prototype._showOrHide = function(checkElement, container) { - var $container = $(container); - - if ($(checkElement).val() !== '0') { - return $container.show(); - } else { - return $container.hide(); + projectVisibility = newProjectVisibility; } - }; - - ProjectNew.prototype.toggleRepoVisibility = function () { - var $repoAccessLevel = $('.js-repo-access-level select'); - var $lfsEnabledOption = $('.js-lfs-enabled select'); - var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; - var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); - var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") - .nextAll() - .hide(); - - $repoAccessLevel.off('change') - .on('change', function () { - var selectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.each(function () { - var $this = $(this); - var repoSelectVal = parseInt($this.val(), 10); - - $this.find('option').enable(); - - if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { - $this.val(selectedVal).trigger('change'); - highlightChanges($this); - } - - $this.find("option[value='" + selectedVal + "']").nextAll().disable(); - }); + }); + } + + toggleSettings() { + this.$selects.each(function () { + var $select = $(this); + var className = $select.data('field') + .replace(/_/g, '-') + .replace('access-level', 'feature'); + ProjectNew._showOrHide($select, '.' + className); + }); + } + + toggleSettingsOnclick() { + this.$selects.on('change', this.toggleSettings); + } + + static _showOrHide(checkElement, container) { + const $container = $(container); + + if ($(checkElement).val() !== '0') { + return $container.show(); + } + return $container.hide(); + } + + toggleRepoVisibility() { + var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); + var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; + var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") + .nextAll() + .hide(); + + $repoAccessLevel + .off('change') + .on('change', function () { + var selectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.each(function () { + var $this = $(this); + var repoSelectVal = parseInt($this.val(), 10); + + $this.find('option').enable(); + + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); + } - if (selectedVal) { - this.$repoSelects.removeClass('disabled'); + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); + }); - if ($lfsEnabledOption.length) { - $lfsEnabledOption.removeClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = ''; - } - } else { - this.$repoSelects.addClass('disabled'); + if (selectedVal) { + this.$repoSelects.removeClass('disabled'); - if ($lfsEnabledOption.length) { - $lfsEnabledOption.val('false').addClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = 'none'; - containerRegistryCheckbox.checked = false; - } + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = ''; } + } else { + this.$repoSelects.addClass('disabled'); - prevSelectedVal = selectedVal; - }.bind(this)); - }; + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = 'none'; + containerRegistryCheckbox.checked = false; + } + } - return ProjectNew; - })(); -}).call(window); + prevSelectedVal = selectedVal; + }.bind(this)); + } +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index bffc85e6315..07a49d1506c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -2,79 +2,73 @@ import Api from './api'; import ProjectSelectComboButton from './project_select_combo_button'; -(function () { - this.ProjectSelect = (function () { - function ProjectSelect() { - $('.ajax-project-select').each(function(i, select) { - var placeholder; - const simpleFilter = $(select).data('simple-filter') || false; - this.groupId = $(select).data('group-id'); - this.includeGroups = $(select).data('include-groups'); - this.allProjects = $(select).data('all-projects') || false; - this.orderBy = $(select).data('order-by') || 'id'; - this.withIssuesEnabled = $(select).data('with-issues-enabled'); - this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); +export default function projectSelect() { + $('.ajax-project-select').each(function(i, select) { + var placeholder; + const simpleFilter = $(select).data('simple-filter') || false; + this.groupId = $(select).data('group-id'); + this.includeGroups = $(select).data('include-groups'); + this.allProjects = $(select).data('all-projects') || false; + this.orderBy = $(select).data('order-by') || 'id'; + this.withIssuesEnabled = $(select).data('with-issues-enabled'); + this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); - placeholder = "Search for project"; - if (this.includeGroups) { - placeholder += " or group"; - } + placeholder = "Search for project"; + if (this.includeGroups) { + placeholder += " or group"; + } - $(select).select2({ - placeholder: placeholder, - minimumInputLength: 0, - query: (function (_this) { - return function (query) { - var finalCallback, projectsCallback; - finalCallback = function (projects) { + $(select).select2({ + placeholder: placeholder, + minimumInputLength: 0, + query: (function (_this) { + return function (query) { + var finalCallback, projectsCallback; + finalCallback = function (projects) { + var data; + data = { + results: projects + }; + return query.callback(data); + }; + if (_this.includeGroups) { + projectsCallback = function (projects) { + var groupsCallback; + groupsCallback = function (groups) { var data; - data = { - results: projects - }; - return query.callback(data); + data = groups.concat(projects); + return finalCallback(data); }; - if (_this.includeGroups) { - projectsCallback = function (projects) { - var groupsCallback; - groupsCallback = function (groups) { - var data; - data = groups.concat(projects); - return finalCallback(data); - }; - return Api.groups(query.term, {}, groupsCallback); - }; - } else { - projectsCallback = finalCallback; - } - if (_this.groupId) { - return Api.groupProjects(_this.groupId, query.term, projectsCallback); - } else { - return Api.projects(query.term, { - order_by: _this.orderBy, - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - membership: !_this.allProjects, - }, projectsCallback); - } + return Api.groups(query.term, {}, groupsCallback); }; - })(this), - id: function(project) { - if (simpleFilter) return project.id; - return JSON.stringify({ - name: project.name, - url: project.web_url, - }); - }, - text: function (project) { - return project.name_with_namespace || project.name; - }, - dropdownCssClass: "ajax-project-dropdown" + } else { + projectsCallback = finalCallback; + } + if (_this.groupId) { + return Api.groupProjects(_this.groupId, query.term, projectsCallback); + } else { + return Api.projects(query.term, { + order_by: _this.orderBy, + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + membership: !_this.allProjects, + }, projectsCallback); + } + }; + })(this), + id: function(project) { + if (simpleFilter) return project.id; + return JSON.stringify({ + name: project.name, + url: project.web_url, }); - if (simpleFilter) return select; - return new ProjectSelectComboButton(select); - }); - } - - return ProjectSelect; - })(); -}).call(window); + }, + text: function (project) { + return project.name_with_namespace || project.name; + }, + dropdownCssClass: "ajax-project-dropdown" + }); + if (simpleFilter) return select; + return new ProjectSelectComboButton(select); + }); +} diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js deleted file mode 100644 index 3a51c1f26ac..00000000000 --- a/app/assets/javascripts/project_show.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife */ - -(function() { - this.ProjectShow = (function() { - function ProjectShow() {} - - return ProjectShow; - })(); -}).call(window); - -// I kept class for future diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js index 4ee2e49306d..567c311f119 100644 --- a/app/assets/javascripts/project_variables.js +++ b/app/assets/javascripts/project_variables.js @@ -1,43 +1,39 @@ -(() => { - const HIDDEN_VALUE_TEXT = '******'; - class ProjectVariables { - constructor() { - this.$revealBtn = $('.js-btn-toggle-reveal-values'); - this.$revealBtn.on('click', this.toggleRevealState.bind(this)); - } +const HIDDEN_VALUE_TEXT = '******'; + +export default class ProjectVariables { + constructor() { + this.$revealBtn = $('.js-btn-toggle-reveal-values'); + this.$revealBtn.on('click', this.toggleRevealState.bind(this)); + } - toggleRevealState(e) { - e.preventDefault(); + toggleRevealState(e) { + e.preventDefault(); - const oldStatus = this.$revealBtn.attr('data-status'); - let newStatus = 'hidden'; - let newAction = 'Reveal Values'; + const oldStatus = this.$revealBtn.attr('data-status'); + let newStatus = 'hidden'; + let newAction = 'Reveal Values'; - if (oldStatus === 'hidden') { - newStatus = 'revealed'; - newAction = 'Hide Values'; - } + if (oldStatus === 'hidden') { + newStatus = 'revealed'; + newAction = 'Hide Values'; + } - this.$revealBtn.attr('data-status', newStatus); + this.$revealBtn.attr('data-status', newStatus); - const $variables = $('.variable-value'); + const $variables = $('.variable-value'); - $variables.each((_, variable) => { - const $variable = $(variable); - let newText = HIDDEN_VALUE_TEXT; + $variables.each((_, variable) => { + const $variable = $(variable); + let newText = HIDDEN_VALUE_TEXT; - if (newStatus === 'revealed') { - newText = $variable.attr('data-value'); - } + if (newStatus === 'revealed') { + newText = $variable.attr('data-value'); + } - $variable.text(newText); - }); + $variable.text(newText); + }); - this.$revealBtn.text(newAction); - } + this.$revealBtn.text(newAction); } - - window.gl = window.gl || {}; - window.gl.ProjectVariables = ProjectVariables; -})(); +} diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index bcdc0fd67b8..bf6fc0ec305 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -2,12 +2,13 @@ // Render Gitlab flavoured Markdown // -// Delegates to syntax highlight and render math +// Delegates to syntax highlight and render math & mermaid diagrams. // (function() { $.fn.renderGFM = function() { this.find('.js-syntax-highlight').syntaxHighlight(); this.find('.js-render-math').renderMath(); + this.find('.js-render-mermaid').renderMermaid(); return this; }; diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js new file mode 100644 index 00000000000..a253601f8e8 --- /dev/null +++ b/app/assets/javascripts/render_mermaid.js @@ -0,0 +1,30 @@ +// Renders diagrams and flowcharts from text using Mermaid in any element with the +// `js-render-mermaid` class. +// +// Example markup: +// +// <pre class="js-render-mermaid"> +// graph TD; +// A-- > B; +// A-- > C; +// B-- > D; +// C-- > D; +// </pre> +// + +import Flash from './flash'; + +$.fn.renderMermaid = function renderMermaid() { + if (this.length === 0) return; + + import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { + mermaid.initialize({ + loadOnStart: false, + theme: 'neutral', + }); + + mermaid.init(undefined, this); + }).catch((err) => { + Flash(`Can't load mermaid module: ${err}`); + }); +}; 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 22a9a34dda3..6ee4d487c0b 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,10 +1,12 @@ <script> import Flash from '../../../flash'; import editForm from './edit_form.vue'; +import Icon from '../../../vue_shared/components/icon.vue'; export default { components: { editForm, + Icon, }, props: { isConfidential: { @@ -26,11 +28,8 @@ export default { }; }, computed: { - faEye() { - const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye'; - return { - [eye]: true, - }; + confidentialityIcon() { + return this.isConfidential ? 'eye-slash' : 'eye'; }, }, methods: { @@ -49,7 +48,11 @@ export default { <template> <div class="block issuable-sidebar-item confidentiality"> <div class="sidebar-collapsed-icon"> - <i class="fa" :class="faEye" aria-hidden="true"></i> + <icon + :name="confidentialityIcon" + :size="16" + aria-hidden="true"> + </icon> </div> <div class="title hide-collapsed"> Confidentiality @@ -70,11 +73,21 @@ export default { :update-confidential-attribute="updateConfidentialAttribute" /> <div v-if="!isConfidential" class="no-value sidebar-item-value"> - <i class="fa fa-eye sidebar-item-icon"></i> + <icon + name="eye" + :size="16" + aria-hidden="true" + class="sidebar-item-icon inline"> + </icon> Not confidential </div> <div v-else class="value sidebar-item-value hide-collapsed"> - <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i> + <icon + name="eye-slash" + :size="16" + aria-hidden="true" + class="sidebar-item-icon inline is-active"> + </icon> This issue is confidential </div> </div> diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index c4b2900e020..9aff53cf8af 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -2,6 +2,7 @@ /* global Flash */ import editForm from './edit_form.vue'; import issuableMixin from '../../../vue_shared/mixins/issuable'; +import Icon from '../../../vue_shared/components/icon.vue'; export default { props: { @@ -35,11 +36,12 @@ export default { components: { editForm, + Icon, }, computed: { - lockIconClass() { - return this.isLocked ? 'fa-lock' : 'fa-unlock'; + lockIcon() { + return this.isLocked ? 'lock' : 'lock-open'; }, isLockDialogOpen() { @@ -66,11 +68,12 @@ export default { <template> <div class="block issuable-sidebar-item lock"> <div class="sidebar-collapsed-icon"> - <i - class="fa" - :class="lockIconClass" + <icon + :name="lockIcon" + :size="16" aria-hidden="true" - ></i> + class="sidebar-item-icon is-active"> + </icon> </div> <div class="title hide-collapsed"> @@ -98,10 +101,12 @@ export default { v-if="isLocked" class="value sidebar-item-value" > - <i + <icon + name="lock" + :size="16" aria-hidden="true" - class="fa fa-lock sidebar-item-icon is-active" - ></i> + class="sidebar-item-icon inline is-active"> + </icon> {{ __('Locked') }} </div> @@ -109,10 +114,12 @@ export default { v-else class="no-value sidebar-item-value hide-collapsed" > - <i + <icon + name="lock-open" + :size="16" aria-hidden="true" - class="fa fa-unlock sidebar-item-icon" - ></i> + class="sidebar-item-icon inline"> + </icon> {{ __('Unlocked') }} </div> </div> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index 4ad3d469f25..25acc099699 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store'; import Mediator from '../../sidebar_mediator'; import eventHub from '../../event_hub'; import Flash from '../../../flash'; +import { __ } from '../../../locale'; import subscriptions from './subscriptions.vue'; export default { @@ -21,7 +22,7 @@ export default { onToggleSubscription() { this.mediator.toggleSubscription() .catch(() => { - Flash('Error occurred when toggling the notification subscription'); + Flash(__('Error occurred when toggling the notification subscription')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index a3a8213d63a..940e1764f3d 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -14,6 +14,10 @@ export default { type: Boolean, required: false, }, + id: { + type: Number, + required: false, + }, }, components: { loadingButton, @@ -32,7 +36,7 @@ export default { }, methods: { toggleSubscription() { - eventHub.$emit('toggleSubscription'); + eventHub.$emit('toggleSubscription', this.id); }, }, }; diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js deleted file mode 100644 index bb4d68fcd49..00000000000 --- a/app/assets/javascripts/subscription.js +++ /dev/null @@ -1,45 +0,0 @@ -class Subscription { - constructor(containerElm) { - this.containerElm = containerElm; - - const subscribeButton = containerElm.querySelector('.js-subscribe-button'); - if (subscribeButton) { - // remove class so we don't bind twice - subscribeButton.classList.remove('js-subscribe-button'); - subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); - } - } - - toggleSubscription(event) { - const button = event.currentTarget; - const buttonSpan = button.querySelector('span'); - if (!buttonSpan || button.classList.contains('disabled')) { - return; - } - button.classList.add('disabled'); - - const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; - const toggleActionUrl = this.containerElm.dataset.url; - - $.post(toggleActionUrl, () => { - button.classList.remove('disabled'); - - // hack to allow this to work with the issue boards Vue object - if (document.querySelector('html').classList.contains('issue-boards-page')) { - gl.issueBoards.boardStoreIssueSet( - 'subscribed', - !gl.issueBoards.BoardsStore.detail.issue.subscribed, - ); - } else { - buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; - } - }); - } - - static bindAll(selector) { - [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); - } -} - -window.gl = window.gl || {}; -window.gl.Subscription = Subscription; diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 37e39ce5477..1ab4c2229ca 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,33 +1,24 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ +export default function subscriptionSelect() { + $('.js-subscription-event').each((i, element) => { + const fieldName = $(element).data('field-name'); -class SubscriptionSelect { - constructor() { - $('.js-subscription-event').each(function(i, el) { - var fieldName; - fieldName = $(el).data("field-name"); - return $(el).glDropdown({ - selectable: true, - fieldName: fieldName, - toggleLabel: (function(_this) { - return function(selected, el, instance) { - var $item, label; - label = 'Subscription'; - $item = instance.dropdown.find('.is-active'); - if ($item.length) { - label = $item.text(); - } - return label; - }; - })(this), - clicked: function(options) { - return options.e.preventDefault(); - }, - id: function(obj, el) { - return $(el).data("id"); + return $(element).glDropdown({ + selectable: true, + fieldName, + toggleLabel(selected, el, instance) { + let label = 'Subscription'; + const $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); } - }); + return label; + }, + clicked(options) { + return options.e.preventDefault(); + }, + id(obj, el) { + return $(el).data('id'); + }, }); - } + }); } - -window.SubscriptionSelect = SubscriptionSelect; diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 2e5f9f1088f..8f116233e72 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -6,10 +6,9 @@ Sample configuration: <icon - :img-src="userAvatarSrc" - :img-alt="tooltipText" - :tooltip-text="tooltipText" - tooltip-placement="top" + name="retry" + :size="32" + css-classes="top" /> */ diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 16c0a8efcd2..564fc5029af 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,4 +1,6 @@ <script> + import Icon from '../../../vue_shared/components/icon.vue'; + export default { props: { isLocked: { @@ -14,12 +16,16 @@ }, }, + components: { + Icon, + }, + computed: { - iconClass() { - return { - 'fa-eye-slash': this.isConfidential, - 'fa-lock': this.isLocked, - }; + warningIcon() { + if (this.isConfidential) return 'eye-slash'; + if (this.isLocked) return 'lock'; + + return ''; }, isLockedAndConfidential() { @@ -30,12 +36,13 @@ </script> <template> <div class="issuable-note-warning"> - <i - aria-hidden="true" - class="fa icon" - :class="iconClass" - v-if="!isLockedAndConfidential" - ></i> + <icon + :name="warningIcon" + :size="16" + class="icon inline" + aria-hidden="true" + v-if="!isLockedAndConfidential"> + </icon> <span v-if="isLockedAndConfidential"> {{ __('This issue is confidential and locked.') }} diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 0cc2653761c..247943f83e6 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -35,6 +35,11 @@ export default { type: String, required: false, }, + containerClass: { + type: String, + required: false, + default: 'btn btn-align-content', + }, }, components: { loadingIcon, @@ -49,9 +54,9 @@ export default { <template> <button - class="btn btn-align-content" @click="onClick" type="button" + :class="containerClass" :disabled="loading || disabled" > <transition name="fade"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index a873e00d0f3..ee50ce27c3d 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -25,6 +25,11 @@ type: String, required: false, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -129,6 +134,7 @@ <markdown-toolbar :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" + :can-attach-file="canAttachFile" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 70f5fc1d664..6c575d8eb49 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -50,7 +50,9 @@ <template> <div class="md-header"> <ul class="nav-links clearfix"> - <li :class="{ active: !previewMarkdown }"> + <li + class="md-header-tab" + :class="{ active: !previewMarkdown }"> <a class="js-write-link" href="#md-write-holder" @@ -59,7 +61,9 @@ Write </a> </li> - <li :class="{ active: previewMarkdown }"> + <li + class="md-header-tab" + :class="{ active: previewMarkdown }"> <a class="js-preview-link" href="#md-preview-holder" @@ -68,56 +72,52 @@ Preview </a> </li> - <li class="pull-right"> - <div class="toolbar-group"> - <toolbar-button - tag="**" - button-title="Add bold text" - icon="bold" /> - <toolbar-button - tag="*" - button-title="Add italic text" - icon="italic" /> - <toolbar-button - tag="> " - :prepend="true" - button-title="Insert a quote" - icon="quote" /> - <toolbar-button - tag="`" - tag-block="```" - button-title="Insert code" - icon="code" /> - <toolbar-button - tag="* " - :prepend="true" - button-title="Add a bullet list" - icon="list-bulleted" /> - <toolbar-button - tag="1. " - :prepend="true" - button-title="Add a numbered list" - icon="list-numbered" /> - <toolbar-button - tag="* [ ] " - :prepend="true" - button-title="Add a task list" - icon="task-done" /> - </div> - <div class="toolbar-group"> - <button - v-tooltip - aria-label="Go full screen" - class="toolbar-btn js-zen-enter" - data-container="body" - tabindex="-1" - title="Go full screen" - type="button"> - <icon - name="screen-full"> - </icon> - </button> - </div> + <li class="md-header-toolbar"> + <toolbar-button + tag="**" + button-title="Add bold text" + icon="bold" /> + <toolbar-button + tag="*" + button-title="Add italic text" + icon="italic" /> + <toolbar-button + tag="> " + :prepend="true" + button-title="Insert a quote" + icon="quote" /> + <toolbar-button + tag="`" + tag-block="```" + button-title="Insert code" + icon="code" /> + <toolbar-button + tag="* " + :prepend="true" + button-title="Add a bullet list" + icon="list-bulleted" /> + <toolbar-button + tag="1. " + :prepend="true" + button-title="Add a numbered list" + icon="list-numbered" /> + <toolbar-button + tag="* [ ] " + :prepend="true" + button-title="Add a task list" + icon="task-done" /> + <button + v-tooltip + aria-label="Go full screen" + class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" + data-container="body" + tabindex="-1" + title="Go full screen" + type="button"> + <icon + name="screen-full"> + </icon> + </button> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 65fe7bbd94e..ea2509d2839 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -9,6 +9,11 @@ type: String, required: false, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> @@ -41,7 +46,10 @@ are supported </template> </div> - <span class="uploading-container"> + <span + v-if="canAttachFile" + class="uploading-container" + > <span class="uploading-progress-container hide"> <i class="fa fa-file-image-o toolbar-button-icon" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index b930fb116a3..e3e41f8f0ca 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -40,7 +40,7 @@ <button v-tooltip type="button" - class="toolbar-btn js-md hidden-xs" + class="toolbar-btn js-md" tabindex="-1" data-container="body" :data-md-tag="tag" diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 10f9e9b70b0..9982a5779af 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -56,6 +56,12 @@ } } +.blank-state-center { + padding-top: 20px; + padding-bottom: 20px; + text-align: center; +} + .blank-state { padding: 20px; border: 1px solid $border-color; @@ -66,7 +72,10 @@ align-items: center; padding: 50px 30px; } +} +.blank-state, +.blank-state-center { .blank-state-icon { svg { display: block; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 9c1439dfad5..91976ca1f56 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -353,3 +353,7 @@ display: -webkit-flex; display: flex; } + +.flex-right { + margin-left: auto; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5f5b5657a2f..5e4ddf366ef 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -2,7 +2,9 @@ .cgray { color: $common-gray; } .clgray { color: $common-gray-light; } .cred { color: $common-red; } +svg.cred { fill: $common-red; } .cgreen { color: $common-green; } +svg.cgreen { fill: $common-green; } .cdark { color: $common-gray-dark; } .text-secondary { color: $gl-text-color-secondary; @@ -428,6 +430,7 @@ img.emoji { /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-5 { margin-top: 5px; } +.prepend-top-8 { margin-top: $grid-size; } .prepend-top-10 { margin-top: 10px; } .prepend-top-15 { margin-top: 15px; } .prepend-top-default { margin-top: $gl-padding !important; } diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index 320f458630a..b73932eb7e1 100644 --- a/app/assets/stylesheets/framework/contextual-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -40,12 +40,6 @@ a:hover { background-color: $link-hover-background; color: $gl-text-color; - - .settings-avatar { - svg { - fill: $gl-text-color; - } - } } .avatar-container { @@ -138,10 +132,6 @@ color: $gl-text-color-secondary; } - svg { - fill: $gl-text-color-secondary; - } - .nav-item-name { flex: 1; } @@ -224,10 +214,6 @@ &:hover { color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } } @@ -338,7 +324,6 @@ align-items: center; svg { - fill: $gl-text-color-secondary; margin-right: 8px; } @@ -349,10 +334,6 @@ &:hover { background-color: $border-color; color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 74b6b31b07e..cf8165eab5b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -305,16 +305,11 @@ color: $gl-text-color; border-color: $dropdown-input-focus-border; outline: none; - - svg { - fill: $gl-text-color; - } } svg { height: 14px; width: 14px; - fill: $gl-text-color-secondary; vertical-align: middle; } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index dc591c06c88..db36e27fa74 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -30,10 +30,6 @@ &.dropdown.open > a { color: $color-900; background-color: $color-alternate; - - svg { - fill: currentColor; - } } &.line-separator { @@ -51,10 +47,6 @@ color: $color-200; > a { - svg { - fill: $color-200; - } - &.header-user-dropdown-toggle { .header-user-avatar { border-color: $color-200; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2218b5705fc..f985a3aea5c 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -235,10 +235,6 @@ opacity: 1; color: $white-light; - svg { - fill: currentColor; - } - &.header-user-dropdown-toggle .header-user-avatar { border-color: $white-light; } @@ -269,14 +265,6 @@ font-size: 20px; } } - - &.active > a, - &.dropdown.open > a { - - svg { - fill: currentColor; - } - } } } } @@ -289,10 +277,6 @@ text-decoration: none; outline: 0; color: $white-light; - - svg { - fill: currentColor; - } } > a { @@ -307,10 +291,6 @@ border-radius: $border-radius-default; height: 32px; font-weight: $gl-font-weight-bold; - - svg { - fill: currentColor; - } } &.line-separator { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index ef864e8f6a9..1ab5e6a93f9 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -1,63 +1,35 @@ .ci-status-icon-success, .ci-status-icon-passed { color: $green-500; - - svg { - fill: $green-500; - } } .ci-status-icon-failed { color: $gl-danger; - - svg { - fill: $gl-danger; - } } .ci-status-icon-pending, .ci-status-icon-failed_with_warnings, .ci-status-icon-success_with_warnings { color: $orange-500; - - svg { - fill: $orange-500; - } } .ci-status-icon-running { color: $blue-400; - - svg { - fill: $blue-400; - } } .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found { color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } .ci-status-icon-created, .ci-status-icon-skipped { color: $gray-darkest; - - svg { - fill: $gray-darkest; - } } .ci-status-icon-manual { color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } .icon-link { diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 6819fd88b7f..78a8e57ddbb 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -27,6 +27,8 @@ } svg { + fill: currentColor; + &.s8 { @include svg-size(8px); } &.s12 { @include svg-size(12px); } &.s16 { @include svg-size(16px); } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index cd6f94fb354..5389eb0a5f2 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -57,6 +57,7 @@ .md-header { .nav-links { a { + width: 100%; padding-top: 0; line-height: 19px; @@ -72,6 +73,28 @@ } } +.md-header-tab { + @media(max-width: $screen-xs-max) { + flex: 1; + width: 100%; + border-bottom: 1px solid $border-color; + text-align: center; + } +} + +.md-header-toolbar { + margin-left: auto; + + @media(max-width: $screen-xs-max) { + flex: none; + display: flex; + justify-content: center; + width: 100%; + padding-top: $gl-padding-top; + padding-bottom: $gl-padding-top; + } +} + .referenced-users { color: $gl-text-color; padding-top: 10px; @@ -126,16 +149,6 @@ } } -.toolbar-group { - float: left; - margin-right: -5px; - margin-left: $gl-padding; - - &:first-child { - margin-left: 0; - } -} - .toolbar-btn { float: left; padding: 0 7px; @@ -158,6 +171,16 @@ } } +.toolbar-fullscreen-btn { + margin-left: $gl-padding; + margin-right: -5px; + + @media(max-width: $screen-xs-max) { + margin-left: 0; + margin-right: 0; + } +} + .atwho-view { overflow-y: auto; overflow-x: hidden; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 33012133b66..e12b5aab381 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -130,14 +130,6 @@ background-color: $color-light; color: $color-dark; border-color: $color-dark; - - svg { - fill: $color-dark; - } - } - - svg { - fill: $color-main; } } diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 5a4d3ba0ee9..dbd3144b9b4 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -57,15 +57,7 @@ padding: 5px; font-size: 36px; - svg { - fill: $gl-text-color; - } - &:hover { color: $black; - - svg { - fill: $black; - } } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 27b10b536a2..f139f4ab650 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -49,6 +49,7 @@ font-size: 12px; border-radius: 0; border: 0; + padding: $grid-size; .bash { display: block; @@ -57,14 +58,13 @@ .top-bar { height: 35px; - display: flex; - justify-content: flex-end; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; position: sticky; position: -webkit-sticky; top: $header-height; + padding: $grid-size; &.affix { top: $header-height; @@ -90,9 +90,6 @@ } .truncated-info { - margin: 0 auto; - align-self: center; - .truncated-info-size { margin: 0 5px; } @@ -118,7 +115,11 @@ .controllers-buttons { color: $gl-text-color; - margin: 0 10px; + margin: 0 $grid-size; + + &:last-child { + margin-right: 0; + } } .btn-scroll.animate { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index bce94e09367..848d7f144dc 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -628,21 +628,46 @@ } .diff-file-changes { - width: 450px; + max-width: 560px; + width: 100%; z-index: 150; @media (min-width: $screen-sm-min) { left: $gl-padding; } - a { + .diff-changed-file { + display: flex; padding-top: 8px; padding-bottom: 8px; + min-width: 0; } - .diff-changed-file { + .diff-file-changed-icon { + margin-top: 2px; + } + + .diff-changed-file-content { display: flex; - align-items: center; + flex-direction: column; + min-width: 0; + } + + .diff-changed-file-name, + .diff-changed-file-path { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .diff-changed-file-path { + direction: rtl; + color: $gl-text-color-tertiary; + } + + .diff-changed-stats { + margin-left: auto; + white-space: nowrap; } } diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index dae8ccdef6c..9cc9e11bcd1 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -1,23 +1,3 @@ -.documentation-index { - h1 { - margin: 0; - } - - h2 { - font-size: 20px; - } - - li { - line-height: 24px; - color: $document-index-color; - - a { - margin-right: 3px; - } - } -} - - .shortcut-mappings { font-size: 12px; color: $help-shortcut-mapping-color; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 760c7c80aff..7a5dab16561 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -6,28 +6,20 @@ } .issuable-warning-icon { - color: $orange-600; background-color: $orange-100; border-radius: $border-radius-default; - padding: 5px; margin: 0 $btn-side-margin 0 0; width: $issuable-warning-size; height: $issuable-warning-size; text-align: center; - &:first-of-type { - margin-right: $issuable-warning-icon-margin; + .icon { + fill: $orange-600; + vertical-align: text-bottom; } -} -.sidebar-item-icon { - border-radius: $border-radius-default; - padding: 5px; - margin: 0 3px 0 -4px; - - &.is-active { - color: $orange-600; - background-color: $orange-50; + &:first-of-type { + margin-right: $issuable-warning-icon-margin; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 89f93a92f2e..1e6992cb65e 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -113,6 +113,8 @@ .icon { margin-right: $issuable-warning-icon-margin; + vertical-align: text-bottom; + fill: $orange-600; } + .md-area { @@ -137,12 +139,24 @@ } } -.sidebar-item-value { - .fa { - background-color: inherit; +.sidebar-item-icon { + border-radius: $border-radius-default; + margin: 0 3px 0 -4px; + vertical-align: middle; + + &.is-active { + fill: $orange-600; } } +.sidebar-collapsed-icon .sidebar-item-icon { + margin: 0; +} + +.sidebar-item-value .sidebar-item-icon { + fill: $theme-gray-700; +} + .sidebar-item-warning-message { line-height: 1.5; padding: 16px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 9537eeeee97..2461b818219 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -547,10 +547,6 @@ ul.notes { width: 16px; top: 0; vertical-align: text-top; - - path { - fill: currentColor; - } } .award-control-icon-positive, @@ -570,10 +566,6 @@ ul.notes { .link-highlight { color: $gl-link-color; fill: $gl-link-color; - - svg { - fill: $gl-link-color; - } } .award-control-icon-neutral { diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 25c80e1f950..ade5ddd147b 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -55,10 +55,6 @@ &:not(span):hover { background-color: rgba($gl-text-color-secondary, .07); } - - svg { - fill: $gl-text-color-secondary; - } } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3be7aee69bc..b2ec491146f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar - before_action :authenticate_user_from_personal_access_token! - before_action :authenticate_user_from_rss_token! + before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :validate_user_service_ticket! before_action :check_password_expiration @@ -97,30 +96,15 @@ class ApplicationController < ActionController::Base # (e.g. tokens) to authenticate the user, whereas Devise sets current_user def auth_user return current_user if current_user.present? + return try(:authenticated_user) end - def authenticate_user_from_personal_access_token! - token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence - - return unless token.present? - - user = User.find_by_personal_access_token(token) + # This filter handles personal access tokens, and atom requests with rss tokens + def authenticate_sessionless_user! + user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user - sessionless_sign_in(user) - end - - # This filter handles authentication for atom request with an rss_token - def authenticate_user_from_rss_token! - return unless request.format.atom? - - token = params[:rss_token].presence - - return unless token.present? - - user = User.find_by_rss_token(token) - - sessionless_sign_in(user) + sessionless_sign_in(user) if user end def log_exception(exception) @@ -212,7 +196,11 @@ class ApplicationController < ActionController::Base end def check_password_expiration - if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + return if session[:impersonator_id] || current_user&.ldap_user? + + password_expires_at = current_user&.password_expires_at + + if password_expires_at && password_expires_at < Time.now return redirect_to new_profile_password_path end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 10e8e54f402..cde1e284d2d 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -44,6 +44,7 @@ class AutocompleteController < ApplicationController if @project.blank? && params[:group_id].present? group = Group.find(params[:group_id]) return render_404 unless can?(current_user, :read_group, group) + group end end @@ -54,6 +55,7 @@ class AutocompleteController < ApplicationController if params[:project_id].present? project = Project.find(params[:project_id]) return render_404 unless can?(current_user, :read_project, project) + project end end diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 737656b3dcc..f8049b20b9f 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -84,6 +84,7 @@ module Boards resource.as_json( only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], labels: true, + sidebar_endpoints: true, include: { project: { only: [:id, :path] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 3c64fd964ff..be2e1b47feb 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -4,7 +4,7 @@ module NotesActions included do before_action :set_polling_interval_header, only: [:index] - before_action :noteable, only: :index + before_action :require_noteable!, only: [:index, :create] before_action :authorize_admin_note!, only: [:update, :destroy] before_action :note_project, only: [:create] end @@ -90,7 +90,7 @@ module NotesActions if note.persisted? attrs[:valid] = true - if noteable.nil? || noteable.discussions_rendered_on_frontend? + if noteable.discussions_rendered_on_frontend? attrs.merge!(note_serializer.represent(note)) else attrs.merge!( @@ -191,7 +191,11 @@ module NotesActions end def noteable - @noteable ||= notes_finder.target || render_404 + @noteable ||= notes_finder.target || @note&.noteable + end + + def require_noteable! + render_404 unless noteable end def last_fetched_at diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 7a7bcb1a3d2..f013d21275e 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -80,7 +80,8 @@ class Groups::MilestonesController < Groups::ApplicationController milestones = MilestonesFinder.new(search_params).execute legacy_milestones = GroupMilestone.build_collection(group, group_projects, params) - milestones + legacy_milestones + @sort = params[:sort] || 'due_date_asc' + MilestoneArray.sort(milestones + legacy_milestones, @sort) end def milestone diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 510813846a4..567957ba2cb 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -4,6 +4,7 @@ class Import::GitlabProjectsController < Import::BaseController def new @namespace = Namespace.find(project_params[:namespace_id]) return render_404 unless current_user.can?(:create_projects, @namespace) + @path = project_params[:path] end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 9612b8d8514..56baa19f864 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if current_user log_audit_event(current_user, with: :saml) # Update SAML identity if data has changed. - identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml) + identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take if identity.nil? current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) redirect_to profile_account_path, notice: 'Authentication method updated' @@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_omniauth if current_user # Add new authentication method - current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) + current_user.identities + .with_extern_uid(oauth['provider'], oauth['uid']) + .first_or_create(extern_uid: oauth['uid']) log_audit_event(current_user, with: oauth['provider']) redirect_to profile_account_path, notice: 'Authentication method updated' else diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 494d412b532..6ff96a3f295 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,12 +22,7 @@ class Projects::CommitController < Projects::ApplicationController apply_diff_view_cookie! respond_to do |format| - format.html do - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599 - Gitlab::GitalyClient.allow_n_plus_1_calls do - render - end - end + format.html { render } format.diff { render text: @commit.to_diff } format.patch { render text: @commit.to_patch } end @@ -112,7 +107,7 @@ class Projects::CommitController < Projects::ApplicationController end def commit - @noteable = @commit ||= @project.commit(params[:id]) + @noteable = @commit ||= @project.commit_by(oid: params[:id]) end def define_commit_vars diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 28920877635..5f4afd2cdee 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -57,6 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end + @commits = @commits.with_pipeline_status @commits = prepare_commits_for_rendering(@commits) end end diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index 47c312ffddf..1a418d0f15a 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -12,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController def metrics return render_404 unless deployment.has_metrics? + @metrics = deployment.metrics if @metrics&.any? render json: @metrics, status: :ok diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index dbc1c8bcc28..f58ee3e9109 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -12,6 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController if group return render_404 unless can?(current_user, :read_group, group) + Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) else flash[:alert] = 'Please select a group.' diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index dbc9106ba6d..28fee0465d5 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -171,6 +171,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue return @issue if defined?(@issue) + # The Sortable default scope causes performance issues when used with find_by @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @note = @project.notes.new(noteable: @issuable) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 480a2dff262..e0f4710175f 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -111,6 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController begin return render_404 unless promote_service.execute(@label) + respond_to do |format| format.html do redirect_to(project_labels_path(@project), diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 32759672b6c..293869345bd 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -54,6 +54,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController name = request.headers['X-Gitlab-Lfs-Tmp'] return if name.include?('/') return unless oid.present? && name.start_with?(oid) + name end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 7d16e77ef66..d60a24d3f1d 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -10,10 +10,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic def show @environment = @merge_request.environments_for(current_user).last - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431 - Gitlab::GitalyClient.allow_n_plus_1_calls do - render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } - end + render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } end def diff_for_path diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 22de6680511..abe4e5245b1 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -80,7 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo def commits # Get commits from repository # or from cache if already merged - @commits = prepare_commits_for_rendering(@merge_request.commits) + @commits = + prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status) render json: { html: view_to_html_string('projects/merge_requests/_commits') } end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index ef7d047b1ad..627cb2bd93c 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -76,6 +76,7 @@ class Projects::NotesController < Projects::ApplicationController def authorize_create_note! return unless noteable.lockable? + access_denied! unless can?(current_user, :create_note, noteable) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index f7a9c98629d..292e4158f8b 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -28,6 +28,7 @@ class Projects::WikisController < Projects::ApplicationController ) else return render('empty') unless can?(current_user, :create_wiki, @project) + @page = WikiPage.new(@project_wiki) @page.title = params[:id] @@ -74,7 +75,11 @@ class Projects::WikisController < Projects::ApplicationController def history @page = @project_wiki.find_page(params[:id]) - unless @page + if @page + @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]), + total_count: @page.count_versions) + .page(params[:page]) + else redirect_to( project_wiki_path(@project, :home), notice: "Page not found" @@ -101,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) + @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15)) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2a473ec0cec..a784c6f402a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -269,6 +269,7 @@ class ProjectsController < Projects::ApplicationController def render_landing_page if can?(current_user, :download_code, @project) return render 'projects/no_repo' unless @project.repository_exists? + render 'projects/empty' if @project.empty_repo? else if @project.wiki_enabled? diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index f9496787b15..c8b4682e6dc 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -20,6 +20,7 @@ class Snippets::NotesController < ApplicationController def snippet PersonalSnippet.find_by(id: params[:snippet_id]) end + alias_method :noteable, :snippet def note_params super.merge(noteable_id: params[:snippet_id]) diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 760166b453f..d975f354a88 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -18,6 +18,7 @@ class PersonalAccessTokensFinder def by_user(tokens) return tokens unless @params[:user] + tokens.where(user: @params[:user]) end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 8ad94d3f723..df590cf47c8 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -30,4 +30,11 @@ module AppearancesHelper render 'shared/logo.svg' end end + + # Skip the 'GitLab' type logo when custom brand logo is set + def brand_header_logo_type + unless brand_item && brand_item.header_logo? + render 'shared/logo_type.svg' + end + end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index cd1ecaadb85..e5d2693b01e 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -231,6 +231,15 @@ module ApplicationSettingsHelper :sign_in_text, :signup_enabled, :terminal_max_session_time, + :throttle_unauthenticated_enabled, + :throttle_unauthenticated_requests_per_period, + :throttle_unauthenticated_period_in_seconds, + :throttle_authenticated_web_enabled, + :throttle_authenticated_web_requests_per_period, + :throttle_authenticated_web_period_in_seconds, + :throttle_authenticated_api_enabled, + :throttle_authenticated_api_requests_per_period, + :throttle_authenticated_api_period_in_seconds, :two_factor_grace_period, :unique_ips_limit_enabled, :unique_ips_limit_per_user, diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 4dd573c61f1..636316da80a 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -6,11 +6,6 @@ # See 'detailed_status?` method and `Gitlab::Ci::Status` module. # module CiStatusHelper - def ci_status_path(pipeline) - project = pipeline.project - project_pipeline_path(project, pipeline) - end - def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 4e4a66e8a02..e82136f0177 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -111,6 +111,7 @@ module DiffHelper def diff_file_old_blob_raw_path(diff_file) sha = diff_file.old_content_sha return unless sha + project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) end @@ -152,11 +153,11 @@ module DiffHelper def diff_file_changed_icon(diff_file) if diff_file.deleted_file? || diff_file.renamed_file? - "minus" + "file-deletion" elsif diff_file.new_file? - "plus" + "file-addition" else - "adjust" + "file-modified" end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 5f11fe62030..878bc9b5c9c 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -24,6 +24,7 @@ module EmailsHelper def action_title(url) return unless url + %w(merge_requests issues commit).each do |action| if url.split("/").include?(action) return "View #{action.humanize.singularize}" diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 2c85d7d7720..9d269cb65d6 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -53,6 +53,7 @@ module MarkupHelper # text, wrapping anything found in the requested link fragment.children.each do |node| next unless node.text? + node.replace(link_to(node.text, url, html_options)) end end @@ -221,7 +222,7 @@ module MarkupHelper data = options[:data].merge({ container: 'body' }) content_tag :button, type: 'button', - class: 'toolbar-btn js-md has-tooltip hidden-xs', + class: 'toolbar-btn js-md has-tooltip', tabindex: -1, data: data, title: options[:title], diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index fde961e2da4..3e42063224e 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -78,6 +78,7 @@ module NotificationsHelper # Create hidden field to send notification setting source to controller def hidden_setting_source_input(notification_setting) return unless notification_setting.source_type + hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 0e106e2c85d..5b2ea38a03d 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -97,6 +97,7 @@ module TreeHelper part_path = part if part_path.empty? next if parts.count > max_links && !parts.last(2).include?(part) + yield(part, part_path) end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 46867d2d974..c3d5628f241 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -150,6 +150,7 @@ module VisibilityLevelHelper def restricted_visibility_levels(show_all = false) return [] if current_user.admin? && !show_all + current_application_settings.restricted_visibility_levels || [] end @@ -159,6 +160,7 @@ module VisibilityLevelHelper def disallowed_visibility_level?(form_model, level) return false unless form_model.respond_to?(:visibility_level_allowed?) + !form_model.visibility_level_allowed?(level) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 5e16badabec..a7e0219b03a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -295,6 +295,15 @@ class ApplicationSetting < ActiveRecord::Base sign_in_text: nil, signup_enabled: Settings.gitlab['signup_enabled'], terminal_max_session_time: 0, + throttle_unauthenticated_enabled: false, + throttle_unauthenticated_requests_per_period: 3600, + throttle_unauthenticated_period_in_seconds: 3600, + throttle_authenticated_web_enabled: false, + throttle_authenticated_web_requests_per_period: 7200, + throttle_authenticated_web_period_in_seconds: 3600, + throttle_authenticated_api_enabled: false, + throttle_authenticated_api_requests_per_period: 7200, + throttle_authenticated_api_period_in_seconds: 3600, two_factor_grace_period: 48, user_default_external: false, polling_interval_multiplier: 1, diff --git a/app/models/blob.rb b/app/models/blob.rb index ad0bc2e2ead..29e762724e3 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -76,12 +76,24 @@ class Blob < SimpleDelegator new(blob, project) end + def self.lazy(project, commit_id, path) + BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader| + project.repository.blobs_at(items.map(&:values)).each do |blob| + loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob + end + end + end + def initialize(blob, project = nil) @project = project super(blob) end + def inspect + "#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>" + end + # Returns the data of the blob. # # If the blob is a text based blob the content is converted to UTF-8 and any @@ -95,7 +107,10 @@ class Blob < SimpleDelegator end def load_all_data! - super(project.repository) if project + # Endpoint needed: gitlab-org/gitaly#756 + Gitlab::GitalyClient.allow_n_plus_1_calls do + super(project.repository) if project + end end def no_highlighting? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1b2b0d17910..1d9f367183e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -317,6 +317,7 @@ module Ci def execute_hooks return unless project + build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :job_hooks) project.execute_services(build_data.dup, :job_hooks) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 19814864e50..ebbefc51a4f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -149,34 +149,70 @@ module Ci end end - # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest, ->(ref = nil) do - max_id = unscope(:select) - .select("max(#{quoted_table_name}.id)") - .group(:ref, :sha) - - if ref - where(ref: ref, id: max_id.where(ref: ref)) - else - where(id: max_id) - end - end scope :internal, -> { where(source: internal_sources) } + # Returns the pipelines in descending order (= newest first), optionally + # limited to a number of references. + # + # ref - The name (or names) of the branch(es)/tag(s) to limit the list of + # pipelines to. + def self.newest_first(ref = nil) + relation = order(id: :desc) + + ref ? relation.where(ref: ref) : relation + end + def self.latest_status(ref = nil) - latest(ref).status + newest_first(ref).pluck(:status).first end def self.latest_successful_for(ref) - success.latest(ref).order(id: :desc).first + newest_first(ref).success.take end def self.latest_successful_for_refs(refs) - success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash| + relation = newest_first(refs).success + + relation.each_with_object({}) do |pipeline, hash| hash[pipeline.ref] ||= pipeline end end + # Returns a Hash containing the latest pipeline status for every given + # commit. + # + # The keys of this Hash are the commit SHAs, the values the statuses. + # + # commits - The list of commit SHAs to get the status for. + # ref - The ref to scope the data to (e.g. "master"). If the ref is not + # given we simply get the latest status for the commits, regardless + # of what refs their pipelines belong to. + def self.latest_status_per_commit(commits, ref = nil) + p1 = arel_table + p2 = arel_table.alias + + # This LEFT JOIN will filter out all but the newest row for every + # combination of (project_id, sha) or (project_id, sha, ref) if a ref is + # given. + cond = p1[:sha].eq(p2[:sha]) + .and(p1[:project_id].eq(p2[:project_id])) + .and(p1[:id].lt(p2[:id])) + + cond = cond.and(p1[:ref].eq(p2[:ref])) if ref + join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond) + + relation = select(:sha, :status) + .where(sha: commits) + .where(p2[:id].eq(nil)) + .joins(join.join_sources) + + relation = relation.where(ref: ref) if ref + + relation.each_with_object({}) do |row, hash| + hash[row[:sha]] = row[:status] + end + end + def self.truncate_sha(sha) sha[0...8] end @@ -300,8 +336,10 @@ module Ci def latest? return false unless ref + commit = project.commit(ref) return false unless commit + commit.sha == sha end @@ -469,7 +507,10 @@ module Ci end def latest_builds_with_artifacts - @latest_builds_with_artifacts ||= builds.latest.with_artifacts + # We purposely cast the builds to an Array here. Because we always use the + # rows if there are more than 0 this prevents us from having to run two + # queries: one to get the count and one to get the rows. + @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a end private diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index ee2e43ee9dd..7fac32466ab 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -56,6 +56,7 @@ module Clusters before_transition any => [:creating] do |provider, transition| operation_id = transition.args.first raise ArgumentError.new('operation_id is required') unless operation_id.present? + provider.operation_id = operation_id end diff --git a/app/models/commit.rb b/app/models/commit.rb index 6dba154a6ea..8401d99a08f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -80,10 +80,11 @@ class Commit @raw = raw_commit @project = project + @statuses = {} end def id - @raw.id + raw.id end def ==(other) @@ -236,11 +237,13 @@ class Commit end def status(ref = nil) - @statuses ||= {} - return @statuses[ref] if @statuses.key?(ref) - @statuses[ref] = pipelines.latest_status(ref) + @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id] + end + + def set_status_for_ref(ref, status) + @statuses[ref] = status end def signature @@ -358,7 +361,7 @@ class Commit @deltas ||= raw.deltas end - def diffs(diff_options = nil) + def diffs(diff_options = {}) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb new file mode 100644 index 00000000000..dd93af9df64 --- /dev/null +++ b/app/models/commit_collection.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# A collection of Commit instances for a specific project and Git reference. +class CommitCollection + include Enumerable + + attr_reader :project, :ref, :commits + + # project - The project the commits belong to. + # commits - The Commit instances to store. + # ref - The name of the ref (e.g. "master"). + def initialize(project, commits, ref = nil) + @project = project + @commits = commits + @ref = ref + end + + def each(&block) + commits.each(&block) + end + + # Sets the pipeline status for every commit. + # + # Setting this status ahead of time removes the need for running a query for + # every commit we're displaying. + def with_pipeline_status + statuses = project.pipelines.latest_status_per_commit(map(&:id), ref) + + each do |commit| + commit.set_status_for_ref(ref, statuses[commit.id]) + end + + self + end + + def respond_to_missing?(message, inc_private = false) + commits.respond_to?(message, inc_private) + end + + # rubocop:disable GitlabSecurity/PublicSend + def method_missing(message, *args, &block) + commits.public_send(message, *args, &block) + end +end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 9adc309a22b..d8394415362 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -98,6 +98,7 @@ module Awardable def create_award_emoji(name, current_user) return unless emoji_awardable? + award_emoji.create(name: normalize_name(name), user: current_user) end diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb index 6a9b52a1ef8..eb9417dc34f 100644 --- a/app/models/fork_network_member.rb +++ b/app/models/fork_network_member.rb @@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base belongs_to :forked_from_project, class_name: 'Project' validates :fork_network, :project, presence: true + + after_destroy :cleanup_fork_network + + private + + def cleanup_fork_network + # Explicitly using `#count` makes sure we have the correct number if the + # relation was loaded in the fork_network. + fork_network.destroy if fork_network.fork_network_members.count == 0 + end end diff --git a/app/models/identity.rb b/app/models/identity.rb index ac8094b610e..ff811e19f8a 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,18 +1,27 @@ class Identity < ActiveRecord::Base include Sortable include CaseSensitivity + belongs_to :user validates :provider, presence: true - validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } + validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false } validates :user_id, uniqueness: { scope: :provider } + scope :with_provider, ->(provider) { where(provider: provider) } scope :with_extern_uid, ->(provider, extern_uid) do - extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap') - where(extern_uid: extern_uid, provider: provider) + iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider) end def ldap? provider.starts_with?('ldap') end + + def self.normalize_uid(provider, uid) + if provider.to_s.starts_with?('ldap') + Gitlab::LDAP::Person.normalize_dn(uid) + else + uid.to_s + end + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index b5abc8f57b0..a9863a50d84 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| - json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user] + if options.key?(:sidebar_endpoints) && project + url_helper = Gitlab::Routing.url_helpers + + json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), + toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)) + end if options.key?(:labels) json[:labels] = labels.as_json( diff --git a/app/models/key.rb b/app/models/key.rb index f119b15c737..815fd1de909 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -27,8 +27,10 @@ class Key < ActiveRecord::Base after_commit :add_to_shell, on: :create after_create :post_create_hook + after_create :refresh_user_cache after_commit :remove_from_shell, on: :destroy after_destroy :post_destroy_hook + after_destroy :refresh_user_cache def key=(value) value&.delete!("\n\r") @@ -76,6 +78,12 @@ class Key < ActiveRecord::Base ) end + def refresh_user_cache + return unless user + + Users::KeysCountService.new(user).refresh_cache + end + def post_destroy_hook SystemHooksService.new.execute_hooks_for(self, :destroy) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 1eda0f9cbbd..5382f5cc627 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -284,8 +284,10 @@ class MergeRequestDiff < ActiveRecord::Base def load_commits commits = st_commits.presence || merge_request_diff_commits + commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) } - commits.map { |commit| Commit.from_hash(commit.to_hash, project) } + CommitCollection + .new(merge_request.source_project, commits, merge_request.source_branch) end def save_diffs diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 47e6b785c39..e01e52131f0 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -256,7 +256,7 @@ class Milestone < ActiveRecord::Base def start_date_should_be_less_than_due_date if due_date <= start_date - errors.add(:start_date, "Can't be greater than due date") + errors.add(:due_date, "must be greater than start date") end end diff --git a/app/models/note.rb b/app/models/note.rb index f9676361072..50c9caf8529 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -110,6 +110,7 @@ class Note < ActiveRecord::Base includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end + scope :with_metadata, -> { includes(:system_note_metadata) } after_initialize :ensure_discussion_id before_validation :nullify_blank_type, :nullify_blank_line_code @@ -169,7 +170,13 @@ class Note < ActiveRecord::Base end def cross_reference? - system? && matches_cross_reference_regex? + return unless system? + + if force_cross_reference_regex_check? + matches_cross_reference_regex? + else + SystemNoteService.cross_reference?(note) + end end def diff_note? @@ -382,4 +389,10 @@ class Note < ActiveRecord::Base def set_discussion_id self.discussion_id ||= discussion_class.discussion_id(self) end + + def force_cross_reference_regex_check? + return unless system? + + SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action) + end end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 43c77f3f2a2..8de42ff9d2e 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -65,6 +65,7 @@ class PagesDomain < ActiveRecord::Base def expired? return false unless x509 + current = Time.new current < x509.not_before || x509.not_after < current end @@ -75,6 +76,7 @@ class PagesDomain < ActiveRecord::Base def subject return unless x509 + x509.subject.to_s end @@ -102,6 +104,7 @@ class PagesDomain < ActiveRecord::Base def validate_pages_domain return unless domain + if domain.downcase.ends_with?(Settings.pages.host.downcase) self.errors.add(:domain, "*.#{Settings.pages.host} is restricted") end @@ -109,6 +112,7 @@ class PagesDomain < ActiveRecord::Base def x509 return unless certificate + @x509 ||= OpenSSL::X509::Certificate.new(certificate) rescue OpenSSL::X509::CertificateError nil @@ -116,6 +120,7 @@ class PagesDomain < ActiveRecord::Base def pkey return unless key + @pkey ||= OpenSSL::PKey::RSA.new(key) rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError nil diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 976d85246a8..768f0a7472e 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -51,8 +51,10 @@ class HipchatService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) + message = create_message(data) return unless message.present? + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index b487378edd2..1c065e1ddbd 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -176,6 +176,7 @@ class JiraService < IssueTrackerService def test_settings return unless client_url.present? + # Test settings by getting the project jira_request { client.ServerInfo.all.attrs } end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 5080acffb3c..bc62972dbb0 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -182,6 +182,7 @@ class KubernetesService < DeploymentService kubeclient.get_pods(namespace: actual_namespace).as_json rescue KubeException => err raise err unless err.error_code == 404 + [] end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 3eecbea8cbf..a0af749a93f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -76,8 +76,8 @@ class ProjectWiki # Returns an Array of Gitlab WikiPage instances or an # empty Array if this Wiki has no pages. - def pages - wiki.pages.map { |page| WikiPage.new(self, page, true) } + def pages(limit: nil) + wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) } end # Finds a page within the repository based on a tile diff --git a/app/models/repository.rb b/app/models/repository.rb index 3a89fa9264b..2bf21cbdcc4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -132,7 +132,8 @@ class Repository commits = Gitlab::Git::Commit.where(options) commits = Commit.decorate(commits, @project) if commits.present? - commits + + CommitCollection.new(project, commits, ref) end def commits_between(from, to) @@ -148,11 +149,14 @@ class Repository end raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled| - if is_enabled - find_commits_by_message_by_gitaly(query, ref, path, limit, offset) - else - find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) - end + commits = + if is_enabled + find_commits_by_message_by_gitaly(query, ref, path, limit, offset) + else + find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) + end + + CommitCollection.new(project, commits, ref) end end @@ -213,11 +217,7 @@ class Repository def branch_exists?(branch_name) return false unless raw_repository - @branch_exists_memo ||= Hash.new do |hash, key| - hash[key] = raw_repository.branch_exists?(key) - end - - @branch_exists_memo[branch_name] + branch_names.include?(branch_name) end def ref_exists?(ref) @@ -242,6 +242,7 @@ class Repository Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ + Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" end end @@ -473,6 +474,11 @@ class Repository nil end + # items is an Array like: [[oid, path], [oid1, path1]] + def blobs_at(items) + raw_repository.batch_blobs(items).map { |blob| Blob.decorate(blob, project) } + end + def root_ref if raw_repository raw_repository.root_ref @@ -662,6 +668,7 @@ class Repository def next_branch(name, opts = {}) branch_ids = self.branch_names.map do |n| next 1 if n == name + result = n.match(/\A#{name}-([0-9]+)\z/) result[1].to_i if result end.compact @@ -990,10 +997,6 @@ class Repository raw_repository.ls_files(actual_ref) end - def gitattribute(path, name) - raw_repository.attributes(path)[name] - end - def copy_gitattributes(ref) actual_ref = ref || root_ref begin diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 1f9f8d7286b..29035480371 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -1,4 +1,14 @@ class SystemNoteMetadata < ActiveRecord::Base + # These notes's action text might contain a reference that is external. + # We should always force a deep validation upon references that are found + # in this note type. + # Other notes can always be safely shown as all its references are + # in the same project (i.e. with the same permissions) + TYPES_WITH_CROSS_REFERENCES = %w[ + commit cross_reference + close duplicate + ].freeze + ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved diff --git a/app/models/user.rb b/app/models/user.rb index ea10e2854d6..f98165754ca 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -170,6 +170,7 @@ class User < ActiveRecord::Base after_save :ensure_namespace_correct after_update :username_changed_hook, if: :username_changed? after_destroy :post_destroy_hook + after_destroy :remove_key_cache after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') } after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } @@ -268,8 +269,7 @@ class User < ActiveRecord::Base end def for_github_id(id) - joins(:identities) - .where(identities: { provider: :github, extern_uid: id.to_s }) + joins(:identities).merge(Identity.with_extern_uid(:github, id)) end # Find a User by their primary email or any associated secondary email @@ -445,6 +445,10 @@ class User < ActiveRecord::Base skip_confirmation! if bool end + def skip_reconfirmation=(bool) + skip_reconfirmation! if bool + end + def generate_reset_token @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token) @@ -624,7 +628,9 @@ class User < ActiveRecord::Base end def require_ssh_key? - keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') + count = Users::KeysCountService.new(self).count + + count.zero? && Gitlab::ProtocolAccess.allowed?('ssh') end def require_password_creation? @@ -886,6 +892,10 @@ class User < ActiveRecord::Base system_hook_service.execute_hooks_for(self, :destroy) end + def remove_key_cache + Users::KeysCountService.new(self).delete_cache + end + def delete_async(deleted_by:, params: {}) block if params[:hard_delete] DeleteUserWorker.perform_async(deleted_by.id, id, params) @@ -1119,6 +1129,7 @@ class User < ActiveRecord::Base # override, from Devise::Validatable def password_required? return false if internal? + super end @@ -1136,6 +1147,7 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 5f710961f95..bdfef677ef3 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -127,19 +127,24 @@ class WikiPage @version ||= @page.version end - # Returns an array of Gitlab Commit instances. - def versions + def versions(options = {}) return [] unless persisted? - wiki.wiki.page_versions(@page.path) + wiki.wiki.page_versions(@page.path, options) end - def commit - versions.first + def count_versions + return [] unless persisted? + + wiki.wiki.count_page_versions(@page.path) + end + + def last_version + @last_version ||= versions(limit: 1).first end def last_commit_sha - commit&.sha + last_version&.sha end # Returns the Date that this latest version was @@ -151,7 +156,7 @@ class WikiPage # Returns boolean True or False if this instance # is an old version of the page. def historical? - @page.historical? && versions.first.sha != version.sha + @page.historical? && last_version.sha != version.sha end # Returns boolean True or False if this instance diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb new file mode 100644 index 00000000000..99cc9a196e6 --- /dev/null +++ b/app/services/base_count_service.rb @@ -0,0 +1,34 @@ +# Base class for services that count a single resource such as the number of +# issues for a project. +class BaseCountService + def relation_for_count + raise( + NotImplementedError, + '"relation_for_count" must be implemented and return an ActiveRecord::Relation' + ) + end + + def count + Rails.cache.fetch(cache_key, raw: raw?) { uncached_count }.to_i + end + + def refresh_cache + Rails.cache.write(cache_key, uncached_count, raw: raw?) + end + + def uncached_count + relation_for_count.count + end + + def delete_cache + Rails.cache.delete(cache_key) + end + + def raw? + false + end + + def cache_key + raise NotImplementedError, 'cache_key must be implemented and return a String' + end +end diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb index 44da87cb00c..e73c6ad6780 100644 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ b/app/services/ci/fetch_kubernetes_token_service.rb @@ -34,6 +34,7 @@ module Ci kubeclient.get_secrets.as_json rescue KubeException => err raise err unless err.error_code == 404 + [] end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 43b539ded53..997d247be46 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -19,6 +19,7 @@ module Labels # We skipped validations during creation. Let's run them now, after deleting conflicting labels raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid? + new_label end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index bc0e7ad4e39..f3b99e1ec8c 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -28,6 +28,7 @@ module MergeRequests def find_target_project return target_project if target_project.present? && can?(current_user, :read_project, target_project) + project.default_merge_request_target end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 1da4dbd9e96..cedfcb50e09 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -10,6 +10,8 @@ module MergeRequests attr_reader :merge_request, :source + delegate :merge_jid, :state, to: :@merge_request + def execute(merge_request) if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService) FfMergeService.new(project, current_user, params).execute(merge_request) @@ -27,6 +29,7 @@ module MergeRequests success end end + log_info("Merge process finished on JID #{merge_jid} with state #{state}") rescue MergeError => e handle_merge_error(log_message: e.message, save_message_on_model: true) end @@ -49,7 +52,9 @@ module MergeRequests def commit message = params[:commit_message] || merge_request.merge_commit_message + log_info("Git merge started on JID #{merge_jid}") commit_id = repository.merge(current_user, source, merge_request, message) + log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}") raise MergeError, 'Conflicts detected during merge' unless commit_id @@ -63,7 +68,9 @@ module MergeRequests end def after_merge + log_info("Post merge started on JID #{merge_jid} with state #{state}") MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) + log_info("Post merge finished on JID #{merge_jid} with state #{state}") if delete_source_branch? DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) @@ -92,6 +99,11 @@ module MergeRequests @merge_request.update(merge_error: log_message) if save_message_on_model end + def log_info(message) + @logger ||= Rails.logger + @logger.info("#{merge_request_info} - #{message}") + end + def merge_request_info merge_request.to_reference(full: true) end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index bd9cfd4e0ea..2187f26d1ed 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -6,14 +6,14 @@ module Milestones check_project_milestone!(milestone) Milestone.transaction do - # Destroy all milestones with same title across projects - destroy_old_milestones(milestone) - group_milestone = clone_project_milestone(milestone) move_children_to_group_milestone(group_milestone) - # Just to be safe + # Destroy all milestones with same title across projects + destroy_old_milestones(milestone) + + # Rollback if milestone is not valid unless group_milestone.valid? raise_error(group_milestone.errors.full_messages.to_sentence) end @@ -35,7 +35,7 @@ module Milestones end def move_children_to_group_milestone(group_milestone) - milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids| + milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids| update_children(group_milestone, milestone_ids) end end @@ -49,7 +49,12 @@ module Milestones create_service = CreateService.new(group, current_user, params) - create_service.execute + milestone = create_service.execute + + # milestone won't be valid here because of duplicated title + milestone.save(validate: false) + + milestone end def update_children(group_milestone, milestone_ids) @@ -65,12 +70,12 @@ module Milestones @group ||= parent.group || raise_error('Project does not belong to a group.') end - def destroy_old_milestones(group_milestone) - Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all + def destroy_old_milestones(milestone) + Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all end def group_project_ids - @group_project_ids ||= group.projects.map(&:id) + @group_project_ids ||= group.projects.pluck(:id) end def raise_error(message) diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb index aa034315280..7e575b2d6f3 100644 --- a/app/services/projects/count_service.rb +++ b/app/services/projects/count_service.rb @@ -1,7 +1,7 @@ module Projects # Base class for the various service classes that count project data (e.g. # issues or forks). - class CountService + class CountService < BaseCountService # The version of the cache format. This should be bumped whenever the # underlying logic changes. This removes the need for explicitly flushing # all caches. @@ -11,29 +11,6 @@ module Projects @project = project end - def relation_for_count - raise( - NotImplementedError, - '"relation_for_count" must be implemented and return an ActiveRecord::Relation' - ) - end - - def count - Rails.cache.fetch(cache_key) { uncached_count } - end - - def refresh_cache - Rails.cache.write(cache_key, uncached_count) - end - - def uncached_count - relation_for_count.count - end - - def delete_cache - Rails.cache.delete(cache_key) - end - def cache_key_name raise( NotImplementedError, diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb index 3a0fa84b868..d9bdf3a8ad7 100644 --- a/app/services/projects/forks_count_service.rb +++ b/app/services/projects/forks_count_service.rb @@ -1,6 +1,6 @@ module Projects # Service class for getting and caching the number of forks of a project. - class ForksCountService < CountService + class ForksCountService < Projects::CountService def relation_for_count @project.forks end diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb index fbf31214c28..e3a20b4c1e4 100644 --- a/app/services/projects/group_links/destroy_service.rb +++ b/app/services/projects/group_links/destroy_service.rb @@ -3,6 +3,7 @@ module Projects class DestroyService < BaseService def execute(group_link) return false unless group_link + group_link.destroy end end diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb index 3c0d186a73c..25de97325e2 100644 --- a/app/services/projects/open_issues_count_service.rb +++ b/app/services/projects/open_issues_count_service.rb @@ -1,7 +1,7 @@ module Projects # Service class for counting and caching the number of open issues of a # project. - class OpenIssuesCountService < CountService + class OpenIssuesCountService < Projects::CountService def relation_for_count # We don't include confidential issues in this number since this would # expose the number of confidential issues to non project members. diff --git a/app/services/projects/open_merge_requests_count_service.rb b/app/services/projects/open_merge_requests_count_service.rb index 2a90f78b90d..77e6448fd5e 100644 --- a/app/services/projects/open_merge_requests_count_service.rb +++ b/app/services/projects/open_merge_requests_count_service.rb @@ -1,7 +1,7 @@ module Projects # Service class for counting and caching the number of open merge requests of # a project. - class OpenMergeRequestsCountService < CountService + class OpenMergeRequestsCountService < Projects::CountService def relation_for_count @project.merge_requests.opened end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 5957f612e84..e5cd6fcdfe3 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -60,21 +60,14 @@ module Projects # Notifications project.send_move_instructions(@old_path) - # Move main repository - # TODO: check storage type and NOOP when not using Legacy - unless move_repo_folder(@old_path, @new_path) - raise TransferError.new('Cannot move project') - end - - # Move wiki repo also if present - # TODO: check storage type and NOOP when not using Legacy - move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") + # Directories on disk + move_project_folders(project) # Move missing group labels to project Labels::TransferService.new(current_user, @old_group, project).execute # Move uploads - Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) + move_project_uploads(project) # Move pages Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) @@ -131,5 +124,30 @@ module Projects def execute_system_hooks SystemHooksService.new.execute_hooks_for(project, :transfer) end + + def move_project_folders(project) + return if project.hashed_storage?(:repository) + + # Move main repository + unless move_repo_folder(@old_path, @new_path) + raise TransferError.new("Cannot move project") + end + + # Disk path is changed; we need to ensure we reload it + project.reload_repository! + + # Move wiki repo also if present + move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") + end + + def move_project_uploads(project) + return if project.hashed_storage?(:attachments) + + Gitlab::UploadsTransfer.new.move_project( + project.path, + @old_namespace.full_path, + @new_namespace.full_path + ) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e946218824c..fe71a405565 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -583,6 +583,10 @@ module SystemNoteService create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action)) end + def cross_reference?(note_text) + note_text =~ /\A#{cross_reference_note_prefix}/i + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index e694c5761da..575853fd66b 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -208,6 +208,7 @@ class TodoService def create_todos(users, attributes) Array(users).map do |user| next if pending_todos(user, attributes).exists? + todo = Todo.create(attributes.merge(user_id: user.id)) user.update_todos_count_cache todo diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb new file mode 100644 index 00000000000..f82d27eded9 --- /dev/null +++ b/app/services/users/keys_count_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Users + # Service class for getting the number of SSH keys that belong to a user. + class KeysCountService < BaseCountService + attr_reader :user + + # user - The User for which to get the number of SSH keys. + def initialize(user) + @user = user + end + + def relation_for_count + user.keys + end + + def raw? + # Since we're storing simple integers we don't need all of the additional + # Marshal data Rails includes by default. + true + end + + def cache_key + "users/key-count-service/#{user.id}" + end + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index d4ba3a028be..f4a5cf75018 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -26,11 +26,15 @@ class FileUploader < GitlabUploader # This is used to build Upload paths dynamically based on the model's current # namespace and path, allowing us to ignore renames or transfers. # - # model - Object that responds to `path_with_namespace` + # model - Object that responds to `full_path` and `disk_path` # # Returns a String without a trailing slash - def self.dynamic_path_segment(model) - File.join(CarrierWave.root, base_dir, model.disk_path) + def self.dynamic_path_segment(project) + if project.hashed_storage?(:attachments) + File.join(CarrierWave.root, base_dir, project.disk_path) + else + File.join(CarrierWave.root, base_dir, project.full_path) + end end attr_accessor :model diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb index 098b16017d2..8c7bb750339 100644 --- a/app/validators/certificate_key_validator.rb +++ b/app/validators/certificate_key_validator.rb @@ -17,6 +17,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator def valid_private_key_pem?(value) return false unless value + pkey = OpenSSL::PKey::RSA.new(value) pkey.private? rescue OpenSSL::PKey::PKeyError diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb index e3d18097f71..5239e70a326 100644 --- a/app/validators/certificate_validator.rb +++ b/app/validators/certificate_validator.rb @@ -17,6 +17,7 @@ class CertificateValidator < ActiveModel::EachValidator def valid_certificate_pem?(value) return false unless value + OpenSSL::X509::Certificate.new(value).present? rescue OpenSSL::X509::CertificateError false diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 935787d1a4a..4a2238fe277 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -43,7 +43,7 @@ = f.hidden_field :header_logo_cache = f.file_field :header_logo, class: "" .hint - Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo + Maximum file size is 1MB. Pages are optimized for a 28px tall header logo .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3a4d5ce0b5c..12658dddc06 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -743,5 +743,56 @@ installations. Set to 0 to completely disable polling. = link_to icon('question-circle'), help_page_path('administration/polling') + %fieldset + %legend User and IP Rate Limits + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_unauthenticated_enabled do + = f.check_box :throttle_unauthenticated_enabled + Enable unauthenticated request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_authenticated_api_enabled do + = f.check_box :throttle_authenticated_api_enabled + Enable authenticated API request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_authenticated_web_enabled do + = f.check_box :throttle_authenticated_web_enabled + Enable authenticated web request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index df2bf27be9d..6d8fad0eb8d 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -99,7 +99,7 @@ %td.build-link - if project - = link_to ci_status_path(build.pipeline) do + = link_to pipeline_path(build.pipeline) do %strong= build.pipeline.short_sha %td.timestamp diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index cb4fc69d5b8..f5f621507b8 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -4,6 +4,7 @@ = render 'shared/milestones_filter', counts: @milestone_states .nav-controls + = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestones, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index eca7fb9ddb1..d758e314d41 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title "Milestones" - page_title "Milestones" -- header_title group_title(@group, "Milestones", group_milestones_path(@group)) %h3.page-title New Milestone diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index d0c2e0b1d69..021de4f0caf 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -29,7 +29,7 @@ .row.prepend-top-default .col-md-8 - .documentation-index + .documentation-index.wiki = markdown(@help_index) .col-md-4 .panel.panel-default diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1eca412aff9..e2407f6a428 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -7,7 +7,7 @@ = link_to root_path, title: 'Dashboard', id: 'logo' do = brand_header_logo %span.logo-text.hidden-xs - = render 'shared/logo_type.svg' + = brand_header_logo_type - if current_user = render "layouts/nav/dashboard" diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index f8a2ea18989..a9431cc4956 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -10,25 +10,23 @@ .md-area .md-header %ul.nav-links.clearfix - %li.active + %li.md-header-tab.active %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write - %li + %li.md-header-tab %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview - %li.pull-right - .toolbar-group - = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) - = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) - = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) - = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) - = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) - = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) - = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) - .toolbar-group - %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } - = sprite_icon("screen-full") + %li.md-header-toolbar + = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) + = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) + = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) + = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) + = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) + = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) + = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) + %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } + = sprite_icon("screen-full") .md-write-holder = yield diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 1d6a0fa38ca..36b28c731a1 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@ - if commit.has_signature? - %a{ href: '#', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } + %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index b6b7aae6f9a..44aa8002f12 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -24,5 +24,5 @@ = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') -%a{ href: '#', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } +%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } = label diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 2de2cf9e38c..dd473ebe580 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -22,9 +22,11 @@ - diff_files.each do |diff_file| %li %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } - = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") - %span.diff-file-changes-path.append-right-5= diff_file.new_path - .pull-right + = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8") + %span.diff-changed-file-content.append-right-8 + %strong.diff-changed-file-name= diff_file.blob.name + %span.diff-changed-file-path.prepend-top-5= diff_file.new_path + %span.diff-changed-stats %span.cgreen< +#{diff_file.added_lines} %span.cred< diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index d7859c9fbeb..add394a6356 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -19,14 +19,15 @@ .environments-container - if @deployments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any deployments right now. - %p.blank-state-text - Define environments in the deploy stage(s) in - %code .gitlab-ci.yml - to track deployments here. - = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" + .blank-state-row + .blank-state-center + %h2.blank-state-title + You don't have any deployments right now. + %p.blank-state-text + Define environments in the deploy stage(s) in + %code .gitlab-ci.yml + to track deployments here. + = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder .ci-table.environments{ role: 'grid' } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b9fec8af4d7..48410ffee21 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -27,9 +27,9 @@ .issuable-meta - if @issue.confidential - = icon('eye-slash', class: 'issuable-warning-icon') + .issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon') - if @issue.discussion_locked? - = icon('lock', class: 'issuable-warning-icon') + .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') = issuable_meta(@issue, @project, "Issue") .issuable-actions.js-issuable-actions @@ -40,7 +40,7 @@ .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can_update_issue - %li= link_to 'Edit', edit_project_issue_path(@project, @issue) + %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit' - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index b5067367802..17ac8a20a30 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -4,7 +4,7 @@ .sidebar-container .blocks-container .block - %strong.prepend-top-10 + %strong.inline.prepend-top-8 = @build.name - if can?(current_user, :update_build, @build) && @build.retryable? = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 2abd2c9e652..1d0aaa47b60 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -57,13 +57,13 @@ .build-trace-container.prepend-top-default .top-bar.js-top-bar - .js-truncated-info.truncated-info.hidden< + .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< Showing last %span.js-truncated-info-size.truncated-info-size>< KiB of log - %a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw - .controllers + .controllers.pull-right - if @build.has_trace? = link_to raw_project_job_path(@project, @build), title: 'Show complete raw', diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 72d5c4961ec..75b3db7e505 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -16,7 +16,7 @@ .issuable-meta - if @merge_request.discussion_locked? - = icon('lock', class: 'issuable-warning-icon') + .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') = issuable_meta(@merge_request, @project, "Merge request") .issuable-actions.js-issuable-actions diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index fd3b8c01b83..da364b58e36 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - @sort ||= sort_value_recently_updated -- page_title _('TagsPage|Tags') +- page_title s_('TagsPage|Tags') - add_to_breadcrumbs("Repository", project_tree_path(@project)) .flex-list{ class: container_class } diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml index 0a1ccbc5f1c..efa16d38f84 100644 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -2,4 +2,4 @@ = link_to wiki_page.title, project_wiki_path(@project, wiki_page) %small (#{wiki_page.format}) .pull-right - %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 9ee09262324..969a1677d9a 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -21,7 +21,7 @@ %th= _("Last updated") %th= _("Format") %tbody - - @page.versions.each_with_index do |version, index| + - @page_versions.each_with_index do |version, index| - commit = version %tr %td @@ -37,5 +37,6 @@ %td %strong = version.format += paginate @page_versions, theme: 'gitlab' = render 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index de15fc99eda..b3b83cee81a 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -11,8 +11,8 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe - #{time_ago_with_tooltip(@page.commit.authored_date)} + = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe + #{time_ago_with_tooltip(@page.last_version.authored_date)} .nav-controls = render 'main_links' diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 233d8c95eda..736afa085e8 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -11,6 +11,7 @@ %li If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. %li - The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. + The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}. + For repositories that take longer, use a clone/push combination. %li To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}. diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 6356e9f92cb..f4a4bfaec54 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,3 +1,5 @@ +- show_create = local_assigns.fetch(:show_create, false) + - show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project) - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml index 9b989c23cab..333dd1a00b4 100644 --- a/app/views/shared/boards/components/sidebar/_notifications.html.haml +++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml @@ -1,7 +1,5 @@ - if current_user - .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" } - %span.issuable-header-text.hide-collapsed.pull-left - Notifications - %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } - %span - {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}} + .block.subscriptions + %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions", + ":subscribed" => "issue.subscribed", + ":id" => "issue.id" } diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 3dd14466994..311fc187e49 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -104,6 +104,7 @@ class IrkerWorker parents = commit.parents # Return old value if there's no new one return push_data['before'] if parents.empty? + # Or return the first parent-commit parents[0].id end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index 269776a1f62..fdbc049c2df 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -39,6 +39,7 @@ class StuckCiJobsWorker def drop_stuck(status, timeout) search(status, timeout) do |build| return unless build.stuck? + drop_build :stuck, build, status, timeout end end diff --git a/changelogs/unreleased/.yml b/changelogs/unreleased/.yml deleted file mode 100644 index acf0bb80c72..00000000000 --- a/changelogs/unreleased/.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove update merge request worker tagging. -merge_request: -author: -type: removed diff --git a/changelogs/unreleased/1312-time-spent-at.yml b/changelogs/unreleased/1312-time-spent-at.yml deleted file mode 100644 index c029497e9ab..00000000000 --- a/changelogs/unreleased/1312-time-spent-at.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added possibility to enter past date in /spend command to log time in the past -merge_request: 3044 -author: g3dinua, LockiStrike -type: changed diff --git a/changelogs/unreleased/14970-suggest-rename-remote.yml b/changelogs/unreleased/14970-suggest-rename-remote.yml deleted file mode 100644 index 68a77eb446d..00000000000 --- a/changelogs/unreleased/14970-suggest-rename-remote.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Suggest to rename the remote for existing repository instructions -merge_request: 14970 -author: helmo42 -type: added diff --git a/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml new file mode 100644 index 00000000000..e3c7ffc8046 --- /dev/null +++ b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml @@ -0,0 +1,5 @@ +--- +title: Adds Rubocop rule for line break after guard clause +merge_request: 15188 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml new file mode 100644 index 00000000000..b217cb44bf7 --- /dev/null +++ b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml @@ -0,0 +1,5 @@ +--- +title: Impersonation no longer gets stuck on password change. +merge_request: 15497 +author: +type: fixed diff --git a/changelogs/unreleased/20666-404-error-issue-assigned-with-issues-disabled.yml b/changelogs/unreleased/20666-404-error-issue-assigned-with-issues-disabled.yml deleted file mode 100644 index 830a275bfd5..00000000000 --- a/changelogs/unreleased/20666-404-error-issue-assigned-with-issues-disabled.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fixes 404 error to 'Issues assigned to me' and 'Issues I've created' when issues - are disabled -merge_request: 15021 -author: Jacopo Beschi @jacopo-beschi -type: fixed diff --git a/changelogs/unreleased/23000-pages-api.yml b/changelogs/unreleased/23000-pages-api.yml deleted file mode 100644 index 9f6fa13dd07..00000000000 --- a/changelogs/unreleased/23000-pages-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add API endpoints for Pages Domains -merge_request: 13917 -author: Travis Miller -type: added diff --git a/changelogs/unreleased/23206-load-participants-async.yml b/changelogs/unreleased/23206-load-participants-async.yml deleted file mode 100644 index 12ab43fb88f..00000000000 --- a/changelogs/unreleased/23206-load-participants-async.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update participants and subscriptions button in issuable sidebar to be async -merge_request: 14836 -author: -type: changed diff --git a/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml b/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml deleted file mode 100644 index 8918c42e3fb..00000000000 --- a/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Issue JWT token with registry:catalog:* scope when requested by GitLab admin -merge_request: 14751 -author: Vratislav Kalenda -type: added diff --git a/changelogs/unreleased/27375-dashboard-activity-performance.yml b/changelogs/unreleased/27375-dashboard-activity-performance.yml deleted file mode 100644 index 87c6197a24d..00000000000 --- a/changelogs/unreleased/27375-dashboard-activity-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve DashboardController#activity.json performance -merge_request: 14985 -author: -type: performance diff --git a/changelogs/unreleased/27654-retry-button.yml b/changelogs/unreleased/27654-retry-button.yml deleted file mode 100644 index 11f3b5eb779..00000000000 --- a/changelogs/unreleased/27654-retry-button.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move retry button in job page to sidebar -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step5.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step5.yml deleted file mode 100644 index 1bff4d6930d..00000000000 --- a/changelogs/unreleased/28202_decrease_abc_threshold_step5.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Decrease ABC threshold to 54.28 -merge_request: 14920 -author: Maxim Rydkin -type: other diff --git a/changelogs/unreleased/30140-restore-readme-only-preference.yml b/changelogs/unreleased/30140-restore-readme-only-preference.yml deleted file mode 100644 index 4b4ee4d5be9..00000000000 --- a/changelogs/unreleased/30140-restore-readme-only-preference.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add readme only option as project view -merge_request: 14900 -author: -type: changed diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml deleted file mode 100644 index 8ecb832041e..00000000000 --- a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step3.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Decrease Perceived Complexity threshold to 14 -merge_request: 14231 -author: Maxim Rydkin -type: other diff --git a/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml b/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml deleted file mode 100644 index daf7ac715bd..00000000000 --- a/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds project_id to pipeline hook data -merge_request: 15044 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/32318-filter-icon.yml b/changelogs/unreleased/32318-filter-icon.yml deleted file mode 100644 index 71e7c2c4dac..00000000000 --- a/changelogs/unreleased/32318-filter-icon.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove filter icon from search bar -merge_request: -author: -type: other diff --git a/changelogs/unreleased/3274-geo-route-whitelisting.yml b/changelogs/unreleased/3274-geo-route-whitelisting.yml deleted file mode 100644 index 43a5af80497..00000000000 --- a/changelogs/unreleased/3274-geo-route-whitelisting.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Tighten up whitelisting of certain Geo routes -merge_request: 15082 -author: -type: fixed diff --git a/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml b/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml deleted file mode 100644 index 816e1f83111..00000000000 --- a/changelogs/unreleased/34284-add-changes-to-issuable-webhook-data.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Include the changes in issuable webhook payloads -merge_request: 14308 -author: -type: added diff --git a/changelogs/unreleased/34600-performance-wiki-pages.yml b/changelogs/unreleased/34600-performance-wiki-pages.yml new file mode 100644 index 00000000000..541ae8f8e60 --- /dev/null +++ b/changelogs/unreleased/34600-performance-wiki-pages.yml @@ -0,0 +1,5 @@ +--- +title: Performance issues when loading large number of wiki pages +merge_request: 15276 +author: +type: performance diff --git a/changelogs/unreleased/34768-fix-issuable-header-wrapping.yml b/changelogs/unreleased/34768-fix-issuable-header-wrapping.yml deleted file mode 100644 index 49195bd4168..00000000000 --- a/changelogs/unreleased/34768-fix-issuable-header-wrapping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix problem with issuable header wrapping when content is too long -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/34841-todos.yml b/changelogs/unreleased/34841-todos.yml deleted file mode 100644 index 37180eefbfc..00000000000 --- a/changelogs/unreleased/34841-todos.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix bad type checking to prevent 0 count badge to be shown -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/34897-delete-branch-after-merge.yml b/changelogs/unreleased/34897-delete-branch-after-merge.yml deleted file mode 100644 index 96631aa95c8..00000000000 --- a/changelogs/unreleased/34897-delete-branch-after-merge.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed 'Removed source branch' checkbox in merge widget being ignored. -merge_request: 14832 -author: -type: fixed diff --git a/changelogs/unreleased/35199-case-insensitive-branches-search.yml b/changelogs/unreleased/35199-case-insensitive-branches-search.yml deleted file mode 100644 index da2729e9e55..00000000000 --- a/changelogs/unreleased/35199-case-insensitive-branches-search.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Case insensitive search for branches -merge_request: 14995 -author: George Andrinopoulos -type: fixed diff --git a/changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml b/changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml deleted file mode 100644 index b03baab4950..00000000000 --- a/changelogs/unreleased/35644-refactor-have-http-status-into-have-gitlab-http-status.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor have_http_status into have_gitlab_http_status -merge_request: 14958 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml b/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml deleted file mode 100644 index 7e2a7222162..00000000000 --- a/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix flash errors showing up on a non configured prometheus integration -merge_request: 35652 -author: -type: fixed diff --git a/changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml b/changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml deleted file mode 100644 index 34bb76195af..00000000000 --- a/changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add metric tagging for sidekiq workers -merge_request: 15111 -author: -type: added diff --git a/changelogs/unreleased/3615-improve-welcome-screen.yml b/changelogs/unreleased/3615-improve-welcome-screen.yml deleted file mode 100644 index 862efddb162..00000000000 --- a/changelogs/unreleased/3615-improve-welcome-screen.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reorganize welcome page for new users -merge_request: -author: -type: other diff --git a/changelogs/unreleased/36160-zindex.yml b/changelogs/unreleased/36160-zindex.yml deleted file mode 100644 index a836744fb41..00000000000 --- a/changelogs/unreleased/36160-zindex.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Decreases z-index of select2 to a lower number of our navigation bar -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36629-35958-add-cluster-application-section.yml b/changelogs/unreleased/36629-35958-add-cluster-application-section.yml deleted file mode 100644 index 0afa53e8642..00000000000 --- a/changelogs/unreleased/36629-35958-add-cluster-application-section.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Add applications section to GKE clusters page to easily install Helm Tiller, - Ingress -merge_request: -author: -type: added diff --git a/changelogs/unreleased/3674-hashed-storage-attachments.yml b/changelogs/unreleased/3674-hashed-storage-attachments.yml deleted file mode 100644 index 41bdb5fa568..00000000000 --- a/changelogs/unreleased/3674-hashed-storage-attachments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Hashed Storage support for Attachments -merge_request: 15068 -author: -type: added diff --git a/changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml b/changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml deleted file mode 100644 index 22651967a40..00000000000 --- a/changelogs/unreleased/37032-get-project-branch-invalid-name-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Get Project Branch API shows an helpful error message on invalid refname -merge_request: 14884 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/37442-api-branches-id-repository-branches-is-calling-gitaly-n-1-times-per-request.yml b/changelogs/unreleased/37442-api-branches-id-repository-branches-is-calling-gitaly-n-1-times-per-request.yml deleted file mode 100644 index 11a11a289bf..00000000000 --- a/changelogs/unreleased/37442-api-branches-id-repository-branches-is-calling-gitaly-n-1-times-per-request.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve performance of the /projects/:id/repository/branches API endpoint -merge_request: 15215 -author: -type: performance diff --git a/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml b/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml deleted file mode 100644 index f6906a3b0e0..00000000000 --- a/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Expose project visibility as CI variable - CI_PROJECT_VISIBILITY -merge_request: 15193 -author: -type: added diff --git a/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml b/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml deleted file mode 100644 index bc93aa1fca4..00000000000 --- a/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace WikiPage::CreateService calls with wiki_page factory in specs -merge_request: 14850 -author: Jacopo Beschi @jacopo-beschi -type: changed diff --git a/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml b/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml deleted file mode 100644 index a7127f49c16..00000000000 --- a/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add a latest_merge_request_diff_id column to merge_requests -merge_request: 15035 -author: -type: performance diff --git a/changelogs/unreleased/37660-match-sidebar-colors.yml b/changelogs/unreleased/37660-match-sidebar-colors.yml deleted file mode 100644 index d5600f453e7..00000000000 --- a/changelogs/unreleased/37660-match-sidebar-colors.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change background color of nav sidebar to match other gl sidebars -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/37824-many-branches-lock-server.yml b/changelogs/unreleased/37824-many-branches-lock-server.yml deleted file mode 100644 index f75f79ec4a0..00000000000 --- a/changelogs/unreleased/37824-many-branches-lock-server.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: While displaying a commit, do not show list of related branches if there are - thousands of branches -merge_request: 14812 -author: -type: other diff --git a/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml b/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml deleted file mode 100644 index 554249a3f88..00000000000 --- a/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Removed extra border radius from .file-editor and .file-holder when editing - a file -merge_request: 14803 -author: Rachel Pipkin -type: fixed diff --git a/changelogs/unreleased/38178-fl-mr-notes-components.yml b/changelogs/unreleased/38178-fl-mr-notes-components.yml deleted file mode 100644 index 244ccfb3071..00000000000 --- a/changelogs/unreleased/38178-fl-mr-notes-components.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Moves placeholders components into shared folder with documentation. Makes - them easier to reuse in MR and Snippets comments -merge_request: -author: -type: other diff --git a/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml b/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml deleted file mode 100644 index 48b92c02505..00000000000 --- a/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't create build failed todos when the job is automatically retried -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/38247-hide-create-mr-button-in-issue-show.yml b/changelogs/unreleased/38247-hide-create-mr-button-in-issue-show.yml deleted file mode 100644 index 57ddd8f8388..00000000000 --- a/changelogs/unreleased/38247-hide-create-mr-button-in-issue-show.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove create MR button from issues when MRs are disabled -merge_request: 15071 -author: George Andrinopoulos -type: fixed diff --git a/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml new file mode 100644 index 00000000000..c73cf8bf60b --- /dev/null +++ b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml @@ -0,0 +1,5 @@ +--- +title: Changed validation error message on wrong milestone dates +merge_request: +author: Xurxo Méndez Pérez +type: fixed diff --git a/changelogs/unreleased/38394-smarter-interval.yml b/changelogs/unreleased/38394-smarter-interval.yml deleted file mode 100644 index ead8c3eca5a..00000000000 --- a/changelogs/unreleased/38394-smarter-interval.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update Merge Request polling so there is only one request at a time -merge_request: 15032 -author: -type: fixed diff --git a/changelogs/unreleased/38395-mr-widget-ci.yml b/changelogs/unreleased/38395-mr-widget-ci.yml deleted file mode 100644 index 5109f1bec44..00000000000 --- a/changelogs/unreleased/38395-mr-widget-ci.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Moves mini graph of pipeline to the end of sentence in MR widget. Cleans HTML - and tests -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/38589-internationalize-tags-page.yml b/changelogs/unreleased/38589-internationalize-tags-page.yml deleted file mode 100644 index 4af3da8c23c..00000000000 --- a/changelogs/unreleased/38589-internationalize-tags-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Internationalized tags page -merge_request: 38589 -author: -type: other diff --git a/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml b/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml deleted file mode 100644 index 9de6e54e3af..00000000000 --- a/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add new diff discussions on MR diffs tab in "realtime" -merge_request: 14981 -author: -type: fixed diff --git a/changelogs/unreleased/38720-sort-admin-runners.yml b/changelogs/unreleased/38720-sort-admin-runners.yml deleted file mode 100644 index b1047644891..00000000000 --- a/changelogs/unreleased/38720-sort-admin-runners.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add sort runners on admin runners -merge_request: 14661 -author: Takuya Noguchi -type: added diff --git a/changelogs/unreleased/38822-oauth-search-case-insensitive.yml b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml new file mode 100644 index 00000000000..d84360b4c5c --- /dev/null +++ b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml @@ -0,0 +1,5 @@ +--- +title: OAuth identity lookups case-insensitive +merge_request: 15312 +author: +type: fixed diff --git a/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml b/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml deleted file mode 100644 index 5e142a2b4cf..00000000000 --- a/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cleanup data-page attribute after each Karma test -merge_request: 14742 -author: -type: fixed diff --git a/changelogs/unreleased/38986-due-date.yml b/changelogs/unreleased/38986-due-date.yml deleted file mode 100644 index 7799b8d297e..00000000000 --- a/changelogs/unreleased/38986-due-date.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix timezone bug in Pikaday and upgrade Pikaday version -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml b/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml deleted file mode 100644 index d142afa3433..00000000000 --- a/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Removed d3.js from the graph and users bundles and used the common_d3 bundle - instead -merge_request: 14826 -author: -type: other diff --git a/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml b/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml deleted file mode 100644 index 4b90d68d80c..00000000000 --- a/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 14830 Move GitLab export option to top of import list when creating a new project -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/39109-reenable-scroll-job.yml b/changelogs/unreleased/39109-reenable-scroll-job.yml deleted file mode 100644 index a771f8f8941..00000000000 --- a/changelogs/unreleased/39109-reenable-scroll-job.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enables scroll to bottom once user has scrolled back to bottom in job log -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39167-async-boards-sidebar.yml b/changelogs/unreleased/39167-async-boards-sidebar.yml new file mode 100644 index 00000000000..dc77f1ad451 --- /dev/null +++ b/changelogs/unreleased/39167-async-boards-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Update Issue Boards to fetch the notification subscription status asynchronously +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/39297-remove-help-text-group-lists.yml b/changelogs/unreleased/39297-remove-help-text-group-lists.yml deleted file mode 100644 index 4773d3c5176..00000000000 --- a/changelogs/unreleased/39297-remove-help-text-group-lists.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove help text from group issues page and group merge requests page -merge_request: 14963 -author: -type: removed diff --git a/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml b/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml deleted file mode 100644 index edf142f0311..00000000000 --- a/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Todos spelled correctly on Todos list page -merge_request: 15015 -author: -type: changed diff --git a/changelogs/unreleased/39419-remove-overzealous-tooltips.yml b/changelogs/unreleased/39419-remove-overzealous-tooltips.yml deleted file mode 100644 index d6cf60bebfa..00000000000 --- a/changelogs/unreleased/39419-remove-overzealous-tooltips.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove overzealous tooltips in projects page tabs -merge_request: 15017 -author: -type: removed diff --git a/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml new file mode 100644 index 00000000000..36c2f789eeb --- /dev/null +++ b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml @@ -0,0 +1,5 @@ +--- +title: Label addition/removal are not going to be redacted wrongfully in the API. +merge_request: 15080 +author: +type: fixed diff --git a/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml new file mode 100644 index 00000000000..fc7c024f95a --- /dev/null +++ b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Add inline editing to issues on mobile +merge_request: 15438 +author: +type: changed diff --git a/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml b/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml deleted file mode 100644 index aebf6363d97..00000000000 --- a/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix overlap of right-sidebar and main content when creating a Wiki page -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml b/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml deleted file mode 100644 index 66939d89d69..00000000000 --- a/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow to disable the Performance Bar -merge_request: 15084 -author: -type: fixed diff --git a/changelogs/unreleased/39573-hashed-storage-backup.yml b/changelogs/unreleased/39573-hashed-storage-backup.yml deleted file mode 100644 index 40ee589c8cc..00000000000 --- a/changelogs/unreleased/39573-hashed-storage-backup.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix gitlab:backup rake for hashed storage based repositories -merge_request: 15400 -author: -type: fixed diff --git a/changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml b/changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml deleted file mode 100644 index bda85ac24e0..00000000000 --- a/changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump carrierwave to 1.2.1 -merge_request: 15072 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/39582-nestingdepth-6.yml b/changelogs/unreleased/39582-nestingdepth-6.yml deleted file mode 100644 index efe15f0a5f3..00000000000 --- a/changelogs/unreleased/39582-nestingdepth-6.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable NestingDepth (level 6) on scss-lint -merge_request: 15073 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/39583-reopen-issue-count-cache.yml b/changelogs/unreleased/39583-reopen-issue-count-cache.yml deleted file mode 100644 index ee35bcbcdae..00000000000 --- a/changelogs/unreleased/39583-reopen-issue-count-cache.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refresh open Issue and Merge Request project counter caches when re-opening. -merge_request: 15085 -author: Rob Ede @robjtede -type: fixed diff --git a/changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml b/changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml deleted file mode 100644 index 9a7109d054e..00000000000 --- a/changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only set Auto-Submitted header once for emails on push -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml b/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml deleted file mode 100644 index 95251b46ecc..00000000000 --- a/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix namespacing for MergeWhenPipelineSucceedsService in MR API -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39649-change-default-size-for-gke-cluster-creation.yml b/changelogs/unreleased/39649-change-default-size-for-gke-cluster-creation.yml deleted file mode 100644 index 6faa30177ad..00000000000 --- a/changelogs/unreleased/39649-change-default-size-for-gke-cluster-creation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change default cluster size to n1-default-2 -merge_request: 39649 -author: Fabio Busatto -type: changed diff --git a/changelogs/unreleased/39668-tooltip-safari.yml b/changelogs/unreleased/39668-tooltip-safari.yml deleted file mode 100644 index 5a0f677cf10..00000000000 --- a/changelogs/unreleased/39668-tooltip-safari.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove native title tooltip in pipeline jobs dropdown in Safari -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39720-group-milestone-sorting.yml b/changelogs/unreleased/39720-group-milestone-sorting.yml new file mode 100644 index 00000000000..15ef87fa567 --- /dev/null +++ b/changelogs/unreleased/39720-group-milestone-sorting.yml @@ -0,0 +1,5 @@ +--- +title: Add dropdown sort to group milestones +merge_request: 15230 +author: George Andrinopoulos +type: added diff --git a/changelogs/unreleased/39757-border-zero-of-scss-lint.yml b/changelogs/unreleased/39757-border-zero-of-scss-lint.yml deleted file mode 100644 index ef0ac6c7df9..00000000000 --- a/changelogs/unreleased/39757-border-zero-of-scss-lint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable BorderZero rule in scss-lint -merge_request: 15168 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml b/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml deleted file mode 100644 index 52b6a267ced..00000000000 --- a/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix double border UI bug on pipelines/environments table and pagination -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39791-when-reopening-an-issue-the-mattermost-notification-has-no-context-to-the-issue.yml b/changelogs/unreleased/39791-when-reopening-an-issue-the-mattermost-notification-has-no-context-to-the-issue.yml deleted file mode 100644 index 143641c6183..00000000000 --- a/changelogs/unreleased/39791-when-reopening-an-issue-the-mattermost-notification-has-no-context-to-the-issue.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Include link to issue in reopen message for Slack and Mattermost notifications -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml new file mode 100644 index 00000000000..8b27c43d15b --- /dev/null +++ b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml @@ -0,0 +1,5 @@ +--- +title: Fix commits page throwing 500 when the multi-file editor was enabled +merge_request: 15502 +author: +type: fixed diff --git a/changelogs/unreleased/39878-commit-pipeline-reads-wrong-key.yml b/changelogs/unreleased/39878-commit-pipeline-reads-wrong-key.yml deleted file mode 100644 index b24edfe0cb9..00000000000 --- a/changelogs/unreleased/39878-commit-pipeline-reads-wrong-key.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix commit pipeline showing wrong status -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml new file mode 100644 index 00000000000..b7a974fd8d9 --- /dev/null +++ b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Set the default gitlab-shell timeout to 3 hours +merge_request: 15292 +author: +type: fixed diff --git a/changelogs/unreleased/40016-log-header.yml b/changelogs/unreleased/40016-log-header.yml new file mode 100644 index 00000000000..f52c2d2a0d5 --- /dev/null +++ b/changelogs/unreleased/40016-log-header.yml @@ -0,0 +1,5 @@ +--- +title: Hide log size for mobile screens +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/40068-runner-sorting-regression.yml b/changelogs/unreleased/40068-runner-sorting-regression.yml deleted file mode 100644 index 6a2bd59d6d6..00000000000 --- a/changelogs/unreleased/40068-runner-sorting-regression.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Revert a regression on runners sorting (!15134) -merge_request: 15341 -author: Takuya Noguchi -type: fixed diff --git a/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml new file mode 100644 index 00000000000..9c308321a19 --- /dev/null +++ b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml @@ -0,0 +1,5 @@ +--- +title: Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive' +merge_request: 15493 +author: +type: fixed diff --git a/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml new file mode 100644 index 00000000000..e5879f89156 --- /dev/null +++ b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml @@ -0,0 +1,5 @@ +--- +title: Fix bitbucket wiki import with hashed storage enabled +merge_request: 15490 +author: +type: fixed diff --git a/changelogs/unreleased/40377-blank-states.yml b/changelogs/unreleased/40377-blank-states.yml new file mode 100644 index 00000000000..7635602c68c --- /dev/null +++ b/changelogs/unreleased/40377-blank-states.yml @@ -0,0 +1,5 @@ +--- +title: Fix blank states using old css +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/4080-align-retry-btn.yml b/changelogs/unreleased/4080-align-retry-btn.yml new file mode 100644 index 00000000000..c7d3997839c --- /dev/null +++ b/changelogs/unreleased/4080-align-retry-btn.yml @@ -0,0 +1,5 @@ +--- +title: Align retry button with job title with new grid size +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/add-changes-count-to-merge-requests-api.yml b/changelogs/unreleased/add-changes-count-to-merge-requests-api.yml deleted file mode 100644 index d0a00fafb52..00000000000 --- a/changelogs/unreleased/add-changes-count-to-merge-requests-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add a count of changes to the merge requests API -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/add-ingress-to-cluster-applications.yml b/changelogs/unreleased/add-ingress-to-cluster-applications.yml deleted file mode 100644 index 0064e8672f8..00000000000 --- a/changelogs/unreleased/add-ingress-to-cluster-applications.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Ingress to available Cluster applications -merge_request: -author: -type: added diff --git a/changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml b/changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml deleted file mode 100644 index eef78cd58f9..00000000000 --- a/changelogs/unreleased/add-lazy-option-to-user-avatar-image-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add lazy option to UserAvatarImage -merge_request: 14895 -author: -type: changed diff --git a/changelogs/unreleased/add-packagist-project-service.yml b/changelogs/unreleased/add-packagist-project-service.yml deleted file mode 100644 index a13d00e91f7..00000000000 --- a/changelogs/unreleased/add-packagist-project-service.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Packagist project service -merge_request: 14493 -author: Matt Coleman -type: added diff --git a/changelogs/unreleased/add-shared-vue-loading-button.yml b/changelogs/unreleased/add-shared-vue-loading-button.yml deleted file mode 100644 index a8904acc4e7..00000000000 --- a/changelogs/unreleased/add-shared-vue-loading-button.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add loading button for new UX paradigm -merge_request: 14883 -author: -type: added diff --git a/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml b/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml deleted file mode 100644 index 19d950b48d6..00000000000 --- a/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Avoid fetching all branches for branch existence checks -merge_request: 14778 -author: -type: changed diff --git a/changelogs/unreleased/api-configure-jira.yml b/changelogs/unreleased/api-configure-jira.yml deleted file mode 100644 index 3ac52d573b0..00000000000 --- a/changelogs/unreleased/api-configure-jira.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Validate username/pw for Jiraservice, require them in the API -merge_request: 15025 -author: Robert Schilling -type: fixed diff --git a/changelogs/unreleased/api-doc-group-statistics.yml b/changelogs/unreleased/api-doc-group-statistics.yml deleted file mode 100644 index 385ff978024..00000000000 --- a/changelogs/unreleased/api-doc-group-statistics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update the groups API documentation -merge_request: 15024 -author: Robert Schilling -type: fixed diff --git a/changelogs/unreleased/backport-workhorse-show-all-refs.yml b/changelogs/unreleased/backport-workhorse-show-all-refs.yml deleted file mode 100644 index 36dd2115152..00000000000 --- a/changelogs/unreleased/backport-workhorse-show-all-refs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support show-all-refs for git over HTTP -merge_request: 14834 -author: -type: added diff --git a/changelogs/unreleased/brand_header_change.yml b/changelogs/unreleased/brand_header_change.yml new file mode 100644 index 00000000000..6ea6e8192a4 --- /dev/null +++ b/changelogs/unreleased/brand_header_change.yml @@ -0,0 +1,5 @@ +--- +title: When a custom header logo is present, don't show GitLab type logo +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/bugfix_banzai_closed_milestones.yml b/changelogs/unreleased/bugfix_banzai_closed_milestones.yml deleted file mode 100644 index 4b5c716ddad..00000000000 --- a/changelogs/unreleased/bugfix_banzai_closed_milestones.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix GFM reference links for closed milestones -merge_request: 15234 -author: Vitaliy @blackst0ne Klachkov -type: fixed diff --git a/changelogs/unreleased/bvl-delete-empty-fork-networks.yml b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml new file mode 100644 index 00000000000..3bbb4cf6e3c --- /dev/null +++ b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml @@ -0,0 +1,5 @@ +--- +title: Clean up empty fork networks +merge_request: 15373 +author: +type: other diff --git a/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml new file mode 100644 index 00000000000..e0895cb5d48 --- /dev/null +++ b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml @@ -0,0 +1,5 @@ +--- +title: Don't move repositories and attachments for projects using hashed storage +merge_request: 15479 +author: +type: other diff --git a/changelogs/unreleased/bvl-fix-group-atom-feed.yml b/changelogs/unreleased/bvl-fix-group-atom-feed.yml deleted file mode 100644 index 48f67db7799..00000000000 --- a/changelogs/unreleased/bvl-fix-group-atom-feed.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix the atom feed for group events -merge_request: 14974 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-free-paths.yml b/changelogs/unreleased/bvl-free-paths.yml deleted file mode 100644 index f15459cc788..00000000000 --- a/changelogs/unreleased/bvl-free-paths.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Free up some reserved group names -merge_request: 15052 -author: -type: other diff --git a/changelogs/unreleased/bvl-group-trees.yml b/changelogs/unreleased/bvl-group-trees.yml deleted file mode 100644 index 9f76eb81627..00000000000 --- a/changelogs/unreleased/bvl-group-trees.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show collapsible project lists -merge_request: 14055 -author: -type: changed diff --git a/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml b/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml deleted file mode 100644 index 48b4051711c..00000000000 --- a/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't use JS to delete memberships from projects and groups -merge_request: 15344 -author: -type: fixed diff --git a/changelogs/unreleased/dm-add-sudo-scope.yml b/changelogs/unreleased/dm-add-sudo-scope.yml deleted file mode 100644 index a0c173ce781..00000000000 --- a/changelogs/unreleased/dm-add-sudo-scope.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Add sudo scope for OAuth and Personal Access Tokens to be used by admins to - impersonate other users on the API -merge_request: -author: -type: added diff --git a/changelogs/unreleased/dm-avatarable-with-asset-host.yml b/changelogs/unreleased/dm-avatarable-with-asset-host.yml deleted file mode 100644 index 6cf8d719afb..00000000000 --- a/changelogs/unreleased/dm-avatarable-with-asset-host.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Always return full avatar URL for private/internal groups/projects when asset - host is set -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/dm-convert-private-tokens.yml b/changelogs/unreleased/dm-convert-private-tokens.yml deleted file mode 100644 index 8f5145c897b..00000000000 --- a/changelogs/unreleased/dm-convert-private-tokens.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Convert private tokens to Personal Access Tokens with sudo scope -merge_request: -author: -type: security diff --git a/changelogs/unreleased/dm-notes-for-commit-id.yml b/changelogs/unreleased/dm-notes-for-commit-id.yml deleted file mode 100644 index 5b83332d82f..00000000000 --- a/changelogs/unreleased/dm-notes-for-commit-id.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Improve performance of commits list by fully using DB index when getting commit - note counts -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/dm-reallow-project-path-ending-in-period.yml b/changelogs/unreleased/dm-reallow-project-path-ending-in-period.yml deleted file mode 100644 index ad41d9b84c3..00000000000 --- a/changelogs/unreleased/dm-reallow-project-path-ending-in-period.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reallow project paths ending in periods -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/dm-remove-private-token-from-interface.yml b/changelogs/unreleased/dm-remove-private-token-from-interface.yml deleted file mode 100644 index 1b8996b08c3..00000000000 --- a/changelogs/unreleased/dm-remove-private-token-from-interface.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove private tokens from web interface and API -merge_request: -author: -type: security diff --git a/changelogs/unreleased/dm-remove-private-token.yml b/changelogs/unreleased/dm-remove-private-token.yml deleted file mode 100644 index d721495721a..00000000000 --- a/changelogs/unreleased/dm-remove-private-token.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove Session API now that private tokens are removed from user API endpoints -merge_request: -author: -type: removed diff --git a/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml b/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml deleted file mode 100644 index 5f6e0cafe88..00000000000 --- a/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable MergeableSelector in scss-lint -merge_request: 12810 -author: Takuya Noguchi diff --git a/changelogs/unreleased/es-module-broadcast_message.yml b/changelogs/unreleased/es-module-broadcast_message.yml deleted file mode 100644 index 031bcc449ae..00000000000 --- a/changelogs/unreleased/es-module-broadcast_message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix unnecessary ajax requests in admin broadcast message form -merge_request: 14853 -author: -type: fixed diff --git a/changelogs/unreleased/expose-job-duration.yml b/changelogs/unreleased/expose-job-duration.yml deleted file mode 100644 index 1fe5d897d47..00000000000 --- a/changelogs/unreleased/expose-job-duration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Expose duration in Job entity -merge_request: 13644 -author: Mehdi Lahmam (@mehlah) -type: added diff --git a/changelogs/unreleased/feature-change-signout-route.yml b/changelogs/unreleased/feature-change-signout-route.yml deleted file mode 100644 index bccb85b3eaf..00000000000 --- a/changelogs/unreleased/feature-change-signout-route.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change 'Sign Out' route from a DELETE to a GET -merge_request: 39708 -author: Joe Marty -type: changed diff --git a/changelogs/unreleased/feature-custom-attributes-on-projects-and-groups.yml b/changelogs/unreleased/feature-custom-attributes-on-projects-and-groups.yml deleted file mode 100644 index 9eae989a270..00000000000 --- a/changelogs/unreleased/feature-custom-attributes-on-projects-and-groups.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support custom attributes on groups and projects -merge_request: 14593 -author: Markus Koller -type: changed diff --git a/changelogs/unreleased/feature-hashed-storage-repo-import.yml b/changelogs/unreleased/feature-hashed-storage-repo-import.yml deleted file mode 100644 index 73c16a99053..00000000000 --- a/changelogs/unreleased/feature-hashed-storage-repo-import.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve GitLab Import rake task to work with Hashed Storage and Subgroups -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml b/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml deleted file mode 100644 index 3d8d0f4fcd1..00000000000 --- a/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Support uml:: and captions in reStructuredText' -merge_request: 15120 -author: Markus Koller -type: changed diff --git a/changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml b/changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml deleted file mode 100644 index 1f36d84092a..00000000000 --- a/changelogs/unreleased/feature-reliable-rspec-with-eval-script.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Get true failure from evalulate_script by checking for element beforehand -merge_request: 14898 -author: -type: fixed diff --git a/changelogs/unreleased/feature-ssh_host_fingerprint.yml b/changelogs/unreleased/feature-ssh_host_fingerprint.yml deleted file mode 100644 index 04f9fd1d6ed..00000000000 --- a/changelogs/unreleased/feature-ssh_host_fingerprint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Automatic configuration settings page -merge_request: 13850 -author: Francisco Lopez -type: added diff --git a/changelogs/unreleased/feature_add_mermaid.yml b/changelogs/unreleased/feature_add_mermaid.yml new file mode 100644 index 00000000000..caeb5d3470d --- /dev/null +++ b/changelogs/unreleased/feature_add_mermaid.yml @@ -0,0 +1,5 @@ +--- +title: 'Add support of Mermaid (generation of diagrams and flowcharts from text)' +merge_request: 15107 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/changelogs/unreleased/feature_change_sort_refs.yml b/changelogs/unreleased/feature_change_sort_refs.yml deleted file mode 100644 index 2dccd87d228..00000000000 --- a/changelogs/unreleased/feature_change_sort_refs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change tags order in refs dropdown -merge_request: 15235 -author: Vitaliy @blackst0ne Klachkov -type: changed diff --git a/changelogs/unreleased/fix-500-on-old-merge-requests.yml b/changelogs/unreleased/fix-500-on-old-merge-requests.yml deleted file mode 100644 index 765d7466819..00000000000 --- a/changelogs/unreleased/fix-500-on-old-merge-requests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 500 errors caused by empty diffs in some discussions -merge_request: 14945 -author: Alexander Popov -type: fixed diff --git a/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml b/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml deleted file mode 100644 index 32cdfba4eec..00000000000 --- a/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Ensure merge requests with lots of version don't time out when searching for - pipelines -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/fix-ci-pipelines-index.yml b/changelogs/unreleased/fix-ci-pipelines-index.yml new file mode 100644 index 00000000000..772093fbef0 --- /dev/null +++ b/changelogs/unreleased/fix-ci-pipelines-index.yml @@ -0,0 +1,5 @@ +--- +title: Update composite pipelines index to include "id" +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/fix-issues-api-list-performance.yml b/changelogs/unreleased/fix-issues-api-list-performance.yml deleted file mode 100644 index 9c180f4d55e..00000000000 --- a/changelogs/unreleased/fix-issues-api-list-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Speed up issues list APIs -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/fix-md-form-tabs-double-click-toggle.yml b/changelogs/unreleased/fix-md-form-tabs-double-click-toggle.yml deleted file mode 100644 index 0ec9bcbcde2..00000000000 --- a/changelogs/unreleased/fix-md-form-tabs-double-click-toggle.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix markdown form tabs toggling preview mode from double clicking write mode - button -merge_request: 15119 -author: -type: fixed diff --git a/changelogs/unreleased/fix-project-select-js-without-button.yml b/changelogs/unreleased/fix-project-select-js-without-button.yml deleted file mode 100644 index 389ca2394f0..00000000000 --- a/changelogs/unreleased/fix-project-select-js-without-button.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use project select dropdown not only as a combobutton -merge_request: 15043 -author: -type: fixed diff --git a/changelogs/unreleased/fix-protected-branches-descriptions.yml b/changelogs/unreleased/fix-protected-branches-descriptions.yml new file mode 100644 index 00000000000..8e233d9defd --- /dev/null +++ b/changelogs/unreleased/fix-protected-branches-descriptions.yml @@ -0,0 +1,5 @@ +--- +title: Clarify wording of protected branch settings for the default branch +merge_request: +author: +type: other diff --git a/changelogs/unreleased/fix-subgroup-autocomplete.yml b/changelogs/unreleased/fix-subgroup-autocomplete.yml deleted file mode 100644 index 4baa2b02f77..00000000000 --- a/changelogs/unreleased/fix-subgroup-autocomplete.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix user autocomplete in subgroups -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-system-hook-docs.yml b/changelogs/unreleased/fix-system-hook-docs.yml deleted file mode 100644 index 393c84a2eff..00000000000 --- a/changelogs/unreleased/fix-system-hook-docs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Clarify system_hook triggers in documentation -merge_request: 14957 -author: Joe Marty -type: other diff --git a/changelogs/unreleased/fix-user-tab-activity-mobile.yml b/changelogs/unreleased/fix-user-tab-activity-mobile.yml deleted file mode 100644 index a7e4fcb4355..00000000000 --- a/changelogs/unreleased/fix-user-tab-activity-mobile.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed user profile activity tab being off-screen on mobile -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/go-get-ssh.yml b/changelogs/unreleased/go-get-ssh.yml deleted file mode 100644 index e485a94c6db..00000000000 --- a/changelogs/unreleased/go-get-ssh.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Returns a ssh url for go-get=1 -merge_request: 14990 -author: gvieira37 -type: fixed diff --git a/changelogs/unreleased/group-new-miletone-breadcrumb.yml b/changelogs/unreleased/group-new-miletone-breadcrumb.yml new file mode 100644 index 00000000000..b82c5b604e8 --- /dev/null +++ b/changelogs/unreleased/group-new-miletone-breadcrumb.yml @@ -0,0 +1,5 @@ +--- +title: Fixed new group milestone breadcrumbs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/hide-pipeline-zero-duration.yml b/changelogs/unreleased/hide-pipeline-zero-duration.yml deleted file mode 100644 index 5d7a0983537..00000000000 --- a/changelogs/unreleased/hide-pipeline-zero-duration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Hides pipeline duration in commit box when it is zero (nil) -merge_request: 14979 -author: gvieira37 -type: fixed diff --git a/changelogs/unreleased/improved-changes-dropdown.yml b/changelogs/unreleased/improved-changes-dropdown.yml new file mode 100644 index 00000000000..f305cbe573b --- /dev/null +++ b/changelogs/unreleased/improved-changes-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Improved diff changed files dropdown design +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/issue-36484.yml b/changelogs/unreleased/issue-36484.yml deleted file mode 100644 index a19126e650f..00000000000 --- a/changelogs/unreleased/issue-36484.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove unnecessary alt-texts from pipeline emails -merge_request: 14602 -author: gernberg -type: fixed diff --git a/changelogs/unreleased/issue_38777.yml b/changelogs/unreleased/issue_38777.yml deleted file mode 100644 index 5c49b2f7879..00000000000 --- a/changelogs/unreleased/issue_38777.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow promoting project milestones to group milestones -merge_request: -author: -type: added diff --git a/changelogs/unreleased/issue_40337.yml b/changelogs/unreleased/issue_40337.yml new file mode 100644 index 00000000000..0cd6c5f46a9 --- /dev/null +++ b/changelogs/unreleased/issue_40337.yml @@ -0,0 +1,5 @@ +--- +title: Fix promoting milestone updating all issuables without milestone +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jej-fs-prevent-push-when-missing-objects.yml b/changelogs/unreleased/jej-fs-prevent-push-when-missing-objects.yml deleted file mode 100644 index 4eeedec2c99..00000000000 --- a/changelogs/unreleased/jej-fs-prevent-push-when-missing-objects.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent git push when LFS objects are missing -merge_request: 13837 -author: -type: added diff --git a/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml b/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml deleted file mode 100644 index 3448b003ee0..00000000000 --- a/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Mobile-friendly table on Admin Runners -merge_request: -author: Takuya Noguchi -type: fixed diff --git a/changelogs/unreleased/merge-requests-schema-cleanup.yml b/changelogs/unreleased/merge-requests-schema-cleanup.yml new file mode 100644 index 00000000000..ccce9b1436c --- /dev/null +++ b/changelogs/unreleased/merge-requests-schema-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: Clean up schema of the "merge_requests" table +merge_request: +author: +type: other diff --git a/changelogs/unreleased/mk-add-user-rate-limits.yml b/changelogs/unreleased/mk-add-user-rate-limits.yml new file mode 100644 index 00000000000..512757da5fc --- /dev/null +++ b/changelogs/unreleased/mk-add-user-rate-limits.yml @@ -0,0 +1,6 @@ +--- +title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits + per user +merge_request: 14708 +author: +type: added diff --git a/changelogs/unreleased/move_markdown_preview_to_concern.yml b/changelogs/unreleased/move_markdown_preview_to_concern.yml deleted file mode 100644 index 036e77610b9..00000000000 --- a/changelogs/unreleased/move_markdown_preview_to_concern.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for markdown preview to group milestones -merge_request: 14806 -author: Vitaliy @blackst0ne Klachkov -type: fixed diff --git a/changelogs/unreleased/multi-file-editor-submodules.yml b/changelogs/unreleased/multi-file-editor-submodules.yml deleted file mode 100644 index b83a50957c5..00000000000 --- a/changelogs/unreleased/multi-file-editor-submodules.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added submodule support in multi-file editor -merge_request: -author: -type: added diff --git a/changelogs/unreleased/multiple-query-prometheus-graphs.yml b/changelogs/unreleased/multiple-query-prometheus-graphs.yml deleted file mode 100644 index 9d09166845e..00000000000 --- a/changelogs/unreleased/multiple-query-prometheus-graphs.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Allow multiple queries in a single Prometheus graph to support additional environments - (Canary, Staging, et al.) -merge_request: 15201 -author: -type: added diff --git a/changelogs/unreleased/new-mr-repo-editor.yml b/changelogs/unreleased/new-mr-repo-editor.yml deleted file mode 100644 index a6c15ee30a9..00000000000 --- a/changelogs/unreleased/new-mr-repo-editor.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Repo Editor: Add option to start a new MR directly from comit section' -merge_request: 14665 -author: -type: added diff --git a/changelogs/unreleased/not-found-in-commits.yml b/changelogs/unreleased/not-found-in-commits.yml deleted file mode 100644 index d5f9ff15a36..00000000000 --- a/changelogs/unreleased/not-found-in-commits.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Renders 404 in commits controller if no commits are found for a given path -merge_request: 14610 -author: Guilherme Vieira -type: fixed diff --git a/changelogs/unreleased/osw-merge-process-logs.yml b/changelogs/unreleased/osw-merge-process-logs.yml new file mode 100644 index 00000000000..d2bb0e09834 --- /dev/null +++ b/changelogs/unreleased/osw-merge-process-logs.yml @@ -0,0 +1,5 @@ +--- +title: Add logs for monitoring the merge process +merge_request: +author: +type: other diff --git a/changelogs/unreleased/pawel-metrics-to-prometheus-33643.yml b/changelogs/unreleased/pawel-metrics-to-prometheus-33643.yml deleted file mode 100644 index abab2e55f90..00000000000 --- a/changelogs/unreleased/pawel-metrics-to-prometheus-33643.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Prometheus equivalent of all InfluxDB metrics -merge_request: 13891 -author: -type: changed diff --git a/changelogs/unreleased/pawel-show_empty_page_when_prometheus_metrics_are_disabled-35639.yml b/changelogs/unreleased/pawel-show_empty_page_when_prometheus_metrics_are_disabled-35639.yml deleted file mode 100644 index 987f7286244..00000000000 --- a/changelogs/unreleased/pawel-show_empty_page_when_prometheus_metrics_are_disabled-35639.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make Prometheus metrics endpoint return empty response when metrics are disabled -merge_request: 14490 -author: -type: changed diff --git a/changelogs/unreleased/ph-multi-file-upload-file.yml b/changelogs/unreleased/ph-multi-file-upload-file.yml deleted file mode 100644 index a2bd3cfe459..00000000000 --- a/changelogs/unreleased/ph-multi-file-upload-file.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow files to uploaded in the multi-file editor -merge_request: -author: -type: added diff --git a/changelogs/unreleased/reduce-queries-for-artifacts-button.yml b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml new file mode 100644 index 00000000000..f2d469b5a80 --- /dev/null +++ b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml @@ -0,0 +1,5 @@ +--- +title: Use arrays in Pipeline#latest_builds_with_artifacts +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/refactor-group_links_controller.yml b/changelogs/unreleased/refactor-group_links_controller.yml deleted file mode 100644 index af3d22c34cb..00000000000 --- a/changelogs/unreleased/refactor-group_links_controller.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor GroupLinksController -merge_request: -author: 15121 -type: other diff --git a/changelogs/unreleased/remove-ensure-ref-fetched-from-controllers.yml b/changelogs/unreleased/remove-ensure-ref-fetched-from-controllers.yml deleted file mode 100644 index 57f54bec1e6..00000000000 --- a/changelogs/unreleased/remove-ensure-ref-fetched-from-controllers.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Stop merge requests from fetching their refs when the data is already available. -merge_request: 15129 -author: -type: removed diff --git a/changelogs/unreleased/replace_explore_projects-feature.yml b/changelogs/unreleased/replace_explore_projects-feature.yml deleted file mode 100644 index 85ef045fb4b..00000000000 --- a/changelogs/unreleased/replace_explore_projects-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace the 'features/explore/projects.feature' spinach test with an rspec analog -merge_request: 14755 -author: Vitaliy @blackst0ne Klachkov -type: other diff --git a/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml b/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml deleted file mode 100644 index c4ed017dacd..00000000000 --- a/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Disable Unicorn sampling in Sidekiq since there are no Unicorn sockets to monitor -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml b/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml deleted file mode 100644 index 96e5195d247..00000000000 --- a/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix broken Members link when relative URL root paths are used -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-memoize-logger.yml b/changelogs/unreleased/sh-memoize-logger.yml deleted file mode 100644 index 1b6567ce72f..00000000000 --- a/changelogs/unreleased/sh-memoize-logger.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Memoize GitLab logger to reduce open file descriptors -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sha-handling.yml b/changelogs/unreleased/sha-handling.yml deleted file mode 100644 index d776edafef5..00000000000 --- a/changelogs/unreleased/sha-handling.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 404 errors in API caused when the branch name had a dot -merge_request: 14462 -author: gvieira37 -type: fixed diff --git a/changelogs/unreleased/skip_confirmation_user_API.yml b/changelogs/unreleased/skip_confirmation_user_API.yml new file mode 100644 index 00000000000..144ccd69e68 --- /dev/null +++ b/changelogs/unreleased/skip_confirmation_user_API.yml @@ -0,0 +1,7 @@ +--- +title: Add email confirmation parameters for user creation and update via API +merge_request: +author: Daniel Juarez +type: added + + diff --git a/changelogs/unreleased/tc-delete-merged-protected-tags-fix.yml b/changelogs/unreleased/tc-delete-merged-protected-tags-fix.yml deleted file mode 100644 index 5d5c39108b0..00000000000 --- a/changelogs/unreleased/tc-delete-merged-protected-tags-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: When deleting merged branches, ignore protected tags -merge_request: 15252 -author: -type: fixed diff --git a/changelogs/unreleased/tc-saml-fix-false-empty.yml b/changelogs/unreleased/tc-saml-fix-false-empty.yml deleted file mode 100644 index 987f596475b..00000000000 --- a/changelogs/unreleased/tc-saml-fix-false-empty.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix SAML error 500 when no groups are defined for user -merge_request: 14913 -author: -type: fixed diff --git a/changelogs/unreleased/tree_item_limit.yml b/changelogs/unreleased/tree_item_limit.yml deleted file mode 100644 index d95c5776075..00000000000 --- a/changelogs/unreleased/tree_item_limit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Truncate tree to max 1,000 items and display notice to users -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/update-fe-i18n-guide.yml b/changelogs/unreleased/update-fe-i18n-guide.yml deleted file mode 100644 index 10bcf7836c6..00000000000 --- a/changelogs/unreleased/update-fe-i18n-guide.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update i18n section in FE docs for marking and interpolation -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/use-git-branch-merged.yml b/changelogs/unreleased/use-git-branch-merged.yml deleted file mode 100644 index 24ec226250c..00000000000 --- a/changelogs/unreleased/use-git-branch-merged.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve branch listing page performance -merge_request: 14729 -author: -type: performance diff --git a/changelogs/unreleased/use-title.yml b/changelogs/unreleased/use-title.yml deleted file mode 100644 index 647e282eb69..00000000000 --- a/changelogs/unreleased/use-title.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use title as placeholder instead of issue title for reusability -merge_request: -author: -type: other diff --git a/changelogs/unreleased/winh-admin-projects-namespace-filter.yml b/changelogs/unreleased/winh-admin-projects-namespace-filter.yml deleted file mode 100644 index 7e906f446b0..00000000000 --- a/changelogs/unreleased/winh-admin-projects-namespace-filter.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make NamespaceSelect change URL when filtering -merge_request: 14888 -author: -type: fixed diff --git a/changelogs/unreleased/winh-i18n-contributors-page.yml b/changelogs/unreleased/winh-i18n-contributors-page.yml deleted file mode 100644 index 9b2611fc4fa..00000000000 --- a/changelogs/unreleased/winh-i18n-contributors-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make contributors page translatable -merge_request: 14915 -author: -type: other diff --git a/changelogs/unreleased/winh-namespace-rename-hooks.yml b/changelogs/unreleased/winh-namespace-rename-hooks.yml deleted file mode 100644 index f5090b03b74..00000000000 --- a/changelogs/unreleased/winh-namespace-rename-hooks.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add system hooks user_rename and group_rename -merge_request: 15123 -author: -type: changed diff --git a/changelogs/unreleased/zj-add-performance-changelog-cat.yml b/changelogs/unreleased/zj-add-performance-changelog-cat.yml deleted file mode 100644 index 3d58044a254..00000000000 --- a/changelogs/unreleased/zj-add-performance-changelog-cat.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Performance improvement as category on the changelog -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-commit-cache.yml b/changelogs/unreleased/zj-commit-cache.yml deleted file mode 100644 index e3afe0ea7ef..00000000000 --- a/changelogs/unreleased/zj-commit-cache.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cache commits fetched from the repository -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-commit-show-n-1.yml b/changelogs/unreleased/zj-commit-show-n-1.yml new file mode 100644 index 00000000000..e536434f74a --- /dev/null +++ b/changelogs/unreleased/zj-commit-show-n-1.yml @@ -0,0 +1,5 @@ +--- +title: Fetch blobs in bulk when generating diffs +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/zj-peek-gitaly.yml b/changelogs/unreleased/zj-peek-gitaly.yml deleted file mode 100644 index bd2f2a07540..00000000000 --- a/changelogs/unreleased/zj-peek-gitaly.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Gitaly metrics to the performance bar -merge_request: -author: -type: other diff --git a/changelogs/unreleased/zj-ruby-2-3-5.yml b/changelogs/unreleased/zj-ruby-2-3-5.yml deleted file mode 100644 index 09ec02417aa..00000000000 --- a/changelogs/unreleased/zj-ruby-2-3-5.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade Ruby to 2.3.5 to include security patches -merge_request: 15099 -author: -type: security diff --git a/config/application.rb b/config/application.rb index 5100ec5d2b7..6436f887d14 100644 --- a/config/application.rb +++ b/config/application.rb @@ -113,7 +113,7 @@ module Gitlab config.action_view.sanitized_allowed_protocols = %w(smb) - config.middleware.insert_before Warden::Manager, Rack::Attack + config.middleware.insert_after Warden::Manager, Rack::Attack # Allow access to GitLab API from other domains config.middleware.insert_before Warden::Manager, Rack::Cors do diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 4004e4d767d..60df92a44fc 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -471,3 +471,35 @@ :why: :versions: [] :when: 2017-10-17 17:46:12.367554000 Z +- - :license + - component-emitter + - MIT + - :who: Winnie Hellmann + :why: package.json does not specify the license (README.md does) + :versions: + - 1.1.2 + :when: 2017-11-13 12:23:10.502463000 Z +- - :license + - json-schema + - BSD + - :who: Winnie Hellmann + :why: https://github.com/kriszyp/json-schema/blob/v0.2.3/package.json#L18-L19 + :versions: + - 0.2.3 + :when: 2017-11-16 12:52:18.286091000 Z +- - :license + - node-forge + - New BSD + - :who: Winnie Hellmann + :why: https://github.com/digitalbazaar/forge/blob/0.6.33/LICENSE + :versions: + - 0.6.33 + :when: 2017-11-16 12:56:17.974767000 Z +- - :license + - sntp + - BSD + - :who: Winnie Hellmann + :why: https://github.com/hueniverse/sntp/blob/v1.0.9/package.json#L28-L29 + :versions: + - 1.0.9 + :when: 2017-11-16 13:02:06.765282000 Z diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 7547ba4a8fa..7f6e68ceed6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -559,8 +559,8 @@ production: &base upload_pack: true receive_pack: true - # Git import/fetch timeout - # git_timeout: 800 + # Git import/fetch timeout, in seconds. Defaults to 3 hours. + # git_timeout: 10800 # If you use non-standard ssh port you need to specify it # ssh_port: 22 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d1156b0c8a8..f7c83f7b0f7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -429,7 +429,7 @@ Settings.gitlab_shell['ssh_port'] ||= 22 Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.__send__(:build_gitlab_shell_ssh_path_prefix) -Settings.gitlab_shell['git_timeout'] ||= 800 +Settings.gitlab_shell['git_timeout'] ||= 10800 # # Workhorse diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb index 35e8b3808e2..6ebaf8834d2 100644 --- a/config/initializers/ar5_batching.rb +++ b/config/initializers/ar5_batching.rb @@ -34,6 +34,7 @@ module ActiveRecord yield yielded_relation break if ids.length < of + batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset)) end end diff --git a/config/initializers/batch_loader.rb b/config/initializers/batch_loader.rb new file mode 100644 index 00000000000..2e2256b0eb9 --- /dev/null +++ b/config/initializers/batch_loader.rb @@ -0,0 +1 @@ +Rails.application.config.middleware.use(BatchLoader::Middleware) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 958859be6cf..051ef93b205 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -236,6 +236,7 @@ Devise.setup do |config| provider['args'][:on_single_sign_out] = lambda do |request| ticket = request.params[:session_index] raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) + Gitlab::OAuth::Session.destroy(:cas3, ticket) true end diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb index 1ebe3c7a742..2fd47a3f4d3 100644 --- a/config/initializers/gollum.rb +++ b/config/initializers/gollum.rb @@ -10,4 +10,32 @@ module Gollum index.send(name, *args) end end + + class Wiki + def pages(treeish = nil, limit: nil) + tree_list((treeish || @ref), limit: limit) + end + + def tree_list(ref, limit: nil) + if (sha = @access.ref_to_sha(ref)) + commit = @access.commit(sha) + tree_map_for(sha).inject([]) do |list, entry| + next list unless @page_class.valid_page_name?(entry.name) + + list << entry.page(self, commit) + break list if limit && list.size >= limit + + list + end + else + [] + end + end + end +end + +Rails.application.configure do + config.after_initialize do + Gollum::Page.per_page = Kaminari.config.default_per_page + end end diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb deleted file mode 100644 index 8a3388a267e..00000000000 --- a/config/initializers/math_lexer.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Touch the lexers so it is registered with Rouge -Rouge::Lexers::Math diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index fddb018e948..e9e1f1c4e9b 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -3,6 +3,7 @@ if Gitlab::LDAP::Config.enabled? Gitlab::LDAP::Config.available_servers.each do |server| # do not redeclare LDAP next if server['provider_name'] == 'ldap' + const_set(server['provider_class'], Class.new(LDAP)) end end diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb deleted file mode 100644 index e8a77b146fa..00000000000 --- a/config/initializers/plantuml_lexer.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Touch the lexers so it is registered with Rouge -Rouge::Lexers::Plantuml diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb index 7f0df8949db..38a9cd68d57 100644 --- a/config/initializers/postgresql_cte.rb +++ b/config/initializers/postgresql_cte.rb @@ -61,11 +61,13 @@ module ActiveRecord def with_values=(values) raise ImmutableRelation if @loaded + @values[:with] = values end def recursive_value=(value) raise ImmutableRelation if @loaded + @values[:recursive] = value end diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb new file mode 100644 index 00000000000..9453df2ec5a --- /dev/null +++ b/config/initializers/rack_attack_global.rb @@ -0,0 +1,61 @@ +module Gitlab::Throttle + def self.settings + Gitlab::CurrentSettings.current_application_settings + end + + def self.unauthenticated_options + limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period } + period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.authenticated_api_options + limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period } + period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.authenticated_web_options + limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period } + period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end +end + +class Rack::Attack + throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req| + Gitlab::Throttle.settings.throttle_unauthenticated_enabled && + req.unauthenticated? && + req.ip + end + + throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req| + Gitlab::Throttle.settings.throttle_authenticated_api_enabled && + req.api_request? && + req.authenticated_user_id + end + + throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| + Gitlab::Throttle.settings.throttle_authenticated_web_enabled && + req.web_request? && + req.authenticated_user_id + end + + class Request + def unauthenticated? + !authenticated_user_id + end + + def authenticated_user_id + Gitlab::Auth::RequestAuthenticator.new(self).user&.id + end + + def api_request? + path.start_with?('/api') + end + + def web_request? + !api_request? + end + end +end diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 383782112a8..96c6d954ff7 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -4,7 +4,7 @@ require './spec/support/test_env' class Gitlab::Seeder::CycleAnalytics def initialize(project, perf: false) @project = project - @user = User.order(:id).last + @user = User.admins.first @issue_count = perf ? 1000 : 5 stub_git_pre_receive! end @@ -77,39 +77,41 @@ class Gitlab::Seeder::CycleAnalytics end def seed! - Sidekiq::Testing.inline! do - issues = create_issues - puts '.' - - # Stage 1 - Timecop.travel 5.days.from_now - add_milestones_and_list_labels(issues) - print '.' - - # Stage 2 - Timecop.travel 5.days.from_now - branches = mention_in_commits(issues) - print '.' - - # Stage 3 - Timecop.travel 5.days.from_now - merge_requests = create_merge_requests_closing_issues(issues, branches) - print '.' - - # Stage 4 - Timecop.travel 5.days.from_now - run_builds(merge_requests) - print '.' - - # Stage 5 - Timecop.travel 5.days.from_now - merge_merge_requests(merge_requests) - print '.' - - # Stage 6 / 7 - Timecop.travel 5.days.from_now - deploy_to_production(merge_requests) - print '.' + Sidekiq::Worker.skipping_transaction_check do + Sidekiq::Testing.inline! do + issues = create_issues + puts '.' + + # Stage 1 + Timecop.travel 5.days.from_now + add_milestones_and_list_labels(issues) + print '.' + + # Stage 2 + Timecop.travel 5.days.from_now + branches = mention_in_commits(issues) + print '.' + + # Stage 3 + Timecop.travel 5.days.from_now + merge_requests = create_merge_requests_closing_issues(issues, branches) + print '.' + + # Stage 4 + Timecop.travel 5.days.from_now + run_builds(merge_requests) + print '.' + + # Stage 5 + Timecop.travel 5.days.from_now + merge_merge_requests(merge_requests) + print '.' + + # Stage 6 / 7 + Timecop.travel 5.days.from_now + deploy_to_production(merge_requests) + print '.' + end end print '.' @@ -123,7 +125,7 @@ class Gitlab::Seeder::CycleAnalytics title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}", description: FFaker::Lorem.sentence, state: 'opened', - assignee: @project.team.users.sample + assignees: [@project.team.users.sample] } Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute @@ -155,7 +157,7 @@ class Gitlab::Seeder::CycleAnalytics issue.project.repository.add_branch(@user, branch_name, 'master') - commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name) + commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name) issue.project.repository.commit(commit_sha) GitPushService.new(issue.project, @@ -210,6 +212,8 @@ class Gitlab::Seeder::CycleAnalytics def deploy_to_production(merge_requests) merge_requests.each do |merge_request| + next unless merge_request.head_pipeline + Timecop.travel 12.hours.from_now job = merge_request.head_pipeline.builds.where.not(environment: nil).last @@ -223,7 +227,14 @@ Gitlab::Seeder.quiet do flag = 'SEED_CYCLE_ANALYTICS' if ENV[flag] - Project.all.each do |project| + Project.find_each do |project| + # This seed naively assumes that every project has a repository, and every + # repository has a `master` branch, which may be the case for a pristine + # GDK seed, but is almost never true for a GDK that's actually had + # development performed on it. + next unless project.repository_exists? + next unless project.repository.commit('master') + seeder = Gitlab::Seeder::CycleAnalytics.new(project) seeder.seed! end diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb index 22bac46e25c..1716b6e8153 100644 --- a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb +++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb index 0f3664c13ef..e4febd1614d 100644 --- a/db/migrate/20160608195742_add_repository_storage_to_projects.rb +++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddRepositoryStorageToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb index 5336b036bca..c58cb957df4 100644 --- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb +++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class SetMissingStageOnCiBuilds < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb index 5dc26f8982a..22c925799a3 100644 --- a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb +++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddRequestAccessEnabledToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb index 4a317646788..4fcb29e1325 100644 --- a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb +++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddRequestAccessEnabledToGroups < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb index abe8e701e23..58f7f2a2841 100644 --- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb +++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb index 7414a28ac97..aec709aaf59 100644 --- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb +++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb @@ -1,7 +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/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb index 0100e30a733..df7d922b816 100644 --- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb +++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb @@ -1,7 +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/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class RemoveProjectsPushesSinceGc < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb index ae37da275fd..27ebe0af33b 100644 --- a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb +++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb index 8d4aefa4365..558a1837c79 100644 --- a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb +++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddTwoFactorColumnsToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb index 7ad01a04815..6d43f346d4f 100644 --- a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb +++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb @@ -1,7 +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/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb index f335e77fb5e..3c5cd95726a 100644 --- a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb +++ b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb index 6c9fe19ca34..807dfcb385d 100644 --- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb +++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb index 7b61e811317..255b5e9c4db 100644 --- a/db/migrate/20170320173259_migrate_assignees.rb +++ b/db/migrate/20170320173259_migrate_assignees.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class MigrateAssignees < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb index b2009b282e9..8423bf13fd9 100644 --- a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb +++ b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb @@ -12,6 +12,7 @@ class RemoveTemporaryCiBuildsIndex < ActiveRecord::Migration def up return unless index_exists?(:ci_builds, :id, name: 'index_for_ci_builds_retried_migration') + remove_concurrent_index(:ci_builds, :id, name: "index_for_ci_builds_retried_migration") end diff --git a/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb new file mode 100644 index 00000000000..55e822752af --- /dev/null +++ b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb @@ -0,0 +1,38 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddGlobalRateLimitsToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :throttle_unauthenticated_enabled, :boolean, default: false, allow_null: false + add_column_with_default :application_settings, :throttle_unauthenticated_requests_per_period, :integer, default: 3600, allow_null: false + add_column_with_default :application_settings, :throttle_unauthenticated_period_in_seconds, :integer, default: 3600, allow_null: false + + add_column_with_default :application_settings, :throttle_authenticated_api_enabled, :boolean, default: false, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_api_requests_per_period, :integer, default: 7200, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_api_period_in_seconds, :integer, default: 3600, allow_null: false + + add_column_with_default :application_settings, :throttle_authenticated_web_enabled, :boolean, default: false, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_web_requests_per_period, :integer, default: 7200, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_web_period_in_seconds, :integer, default: 3600, allow_null: false + end + + def down + remove_column :application_settings, :throttle_authenticated_web_period_in_seconds + remove_column :application_settings, :throttle_authenticated_web_requests_per_period + remove_column :application_settings, :throttle_authenticated_web_enabled + + remove_column :application_settings, :throttle_authenticated_api_period_in_seconds + remove_column :application_settings, :throttle_authenticated_api_requests_per_period + remove_column :application_settings, :throttle_authenticated_api_enabled + + remove_column :application_settings, :throttle_unauthenticated_period_in_seconds + remove_column :application_settings, :throttle_unauthenticated_requests_per_period + remove_column :application_settings, :throttle_unauthenticated_enabled + end +end diff --git a/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb new file mode 100644 index 00000000000..021eaa04a0c --- /dev/null +++ b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb @@ -0,0 +1,43 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsAuthorIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_authors + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.author_id = users.id)') + .where('author_id IS NOT NULL') + end + end + + def up + # Replacing the ghost user ID logic would be too complex, hence we don't + # redefine the User model here. + ghost_id = User.select(:id).ghost.id + + MergeRequest.with_orphaned_authors.each_batch(of: 100) do |batch| + batch.update_all(author_id: ghost_id) + end + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :author_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key(:merge_requests, column: :author_id) + end +end diff --git a/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb new file mode 100644 index 00000000000..1a242f01051 --- /dev/null +++ b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsAssigneeIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_assignees + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.assignee_id = users.id)') + .where('assignee_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_assignees.each_batch(of: 100) do |batch| + batch.update_all(assignee_id: nil) + end + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :assignee_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key(:merge_requests, column: :assignee_id) + end +end diff --git a/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb new file mode 100644 index 00000000000..eb3872e38da --- /dev/null +++ b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.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 MergeRequestsUpdatedByIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_updaters + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.updated_by_id = users.id)') + .where('updated_by_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_updaters.each_batch(of: 100) do |batch| + batch.update_all(updated_by_id: nil) + end + + add_concurrent_index( + :merge_requests, + :updated_by_id, + where: 'updated_by_id IS NOT NULL' + ) + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :updated_by_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :updated_by_id) + remove_concurrent_index(:merge_requests, :updated_by_id) + end +end diff --git a/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb new file mode 100644 index 00000000000..925b3e537d7 --- /dev/null +++ b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.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 MergeRequestsMergeUserIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_mergers + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.merge_user_id = users.id)') + .where('merge_user_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_mergers.each_batch(of: 100) do |batch| + batch.update_all(merge_user_id: nil) + end + + add_concurrent_index( + :merge_requests, + :merge_user_id, + where: 'merge_user_id IS NOT NULL' + ) + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :merge_user_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :merge_user_id) + remove_concurrent_index(:merge_requests, :merge_user_id) + end +end diff --git a/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb new file mode 100644 index 00000000000..99740f64fe6 --- /dev/null +++ b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb @@ -0,0 +1,45 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsSourceProjectIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_source_projects + where('NOT EXISTS (SELECT true FROM projects WHERE merge_requests.source_project_id = projects.id)') + .where('source_project_id IS NOT NULL') + end + end + + def up + # We need to allow NULL values so we can nullify the column when the source + # project is removed. We _don't_ want to remove the merge request, instead + # the application will keep them but close them. + change_column_null(:merge_requests, :source_project_id, true) + + MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch| + batch.update_all(source_project_id: nil) + end + + add_concurrent_foreign_key( + :merge_requests, + :projects, + column: :source_project_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :source_project_id) + change_column_null(:merge_requests, :source_project_id, false) + end +end diff --git a/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb new file mode 100644 index 00000000000..c005cf7d173 --- /dev/null +++ b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsMilestoneIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_milestones + where('NOT EXISTS (SELECT true FROM milestones WHERE merge_requests.milestone_id = milestones.id)') + .where('milestone_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_milestones.each_batch(of: 100) do |batch| + batch.update_all(milestone_id: nil) + end + + add_concurrent_foreign_key( + :merge_requests, + :milestones, + column: :milestone_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :milestone_id) + end +end diff --git a/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb new file mode 100644 index 00000000000..5a8ae6e4b57 --- /dev/null +++ b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb @@ -0,0 +1,35 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CiPipelinesIndexOnProjectIdRefStatusId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + TABLE = :ci_pipelines + OLD_COLUMNS = %i[project_id ref status].freeze + NEW_COLUMNS = %i[project_id ref status id].freeze + + def up + unless index_exists?(TABLE, NEW_COLUMNS) + add_concurrent_index(TABLE, NEW_COLUMNS) + end + + if index_exists?(TABLE, OLD_COLUMNS) + remove_concurrent_index(TABLE, OLD_COLUMNS) + end + end + + def down + unless index_exists?(TABLE, OLD_COLUMNS) + add_concurrent_index(TABLE, OLD_COLUMNS) + end + + if index_exists?(TABLE, NEW_COLUMNS) + remove_concurrent_index(TABLE, NEW_COLUMNS) + end + end +end diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb index 82f8147547e..f1f81691f81 100644 --- a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb +++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb index 01fff680183..49fd46b0262 100644 --- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class ResetRelativePositionForIssue < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb index cb1b4f1855d..78413a608f1 100644 --- a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb +++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb index f2ce25d4524..0ab3d61730d 100644 --- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb +++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb @@ -14,6 +14,7 @@ class CleanUploadSymlinks < ActiveRecord::Migration DIRECTORIES_TO_MOVE.each do |dir| symlink_location = File.join(old_upload_dir, dir) next unless File.symlink?(symlink_location) + say "removing symlink: #{symlink_location}" FileUtils.rm(symlink_location) end diff --git a/db/post_migrate/20170406142253_migrate_user_project_view.rb b/db/post_migrate/20170406142253_migrate_user_project_view.rb index c4e910b3b44..d6061dd416d 100644 --- a/db/post_migrate/20170406142253_migrate_user_project_view.rb +++ b/db/post_migrate/20170406142253_migrate_user_project_view.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb index 765daa0a347..bba37e32c01 100644 --- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb +++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers 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 9d9f36550e7..b0b58ab3011 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class UpdateRetriedForCiBuild < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb index f77078ddd70..81e9d050668 100644 --- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb +++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb index c78beda9d21..3e952980866 100644 --- a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb +++ b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb index 97cb242415d..31a73bb3b27 100644 --- a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb +++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class MigrateBuildStageReferenceAgain < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb index 2b79a87ccd8..c735dc67f44 100644 --- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb +++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb @@ -32,6 +32,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration file_name = upload['path'].split('/')[1] next unless move_file(upload['model_id'], secret, file_name) + update_markdown(upload['model_id'], secret, file_name, upload['description']) end end diff --git a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb index acb895e426f..17849b78ceb 100644 --- a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb +++ b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb @@ -13,6 +13,7 @@ class CleanAppearanceSymlinks < ActiveRecord::Migration symlink_location = File.join(old_upload_dir, dir) return unless File.symlink?(symlink_location) + say "removing symlink: #{symlink_location}" FileUtils.rm(symlink_location) end diff --git a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb index a238216253b..b040c81b316 100644 --- a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb +++ b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170927112319_update_notes_type_for_import.rb b/db/post_migrate/20170927112319_update_notes_type_for_import.rb index 1e70acd9868..5a400c71b02 100644 --- a/db/post_migrate/20170927112319_update_notes_type_for_import.rb +++ b/db/post_migrate/20170927112319_update_notes_type_for_import.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class UpdateNotesTypeForImport < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb b/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb deleted file mode 100644 index a7ebbbf34c0..00000000000 --- a/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb +++ /dev/null @@ -1,27 +0,0 @@ -class PopulateMergeRequestsLatestMergeRequestDiffId < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - BATCH_SIZE = 1_000 - - class MergeRequest < ActiveRecord::Base - self.table_name = 'merge_requests' - - include ::EachBatch - end - - disable_ddl_transaction! - - def up - update = ' - latest_merge_request_diff_id = ( - SELECT MAX(id) - FROM merge_request_diffs - WHERE merge_requests.id = merge_request_diffs.merge_request_id - )'.squish - - MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation| - relation.update_all(update) - end - end -end diff --git a/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb new file mode 100644 index 00000000000..7a63382cc6d --- /dev/null +++ b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb @@ -0,0 +1,29 @@ +class ScheduleMergeRequestLatestMergeRequestDiffIdMigrations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 50_000 + MIGRATION = 'PopulateMergeRequestsLatestMergeRequestDiffId' + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include ::EachBatch + end + + # On GitLab.com, we saw that we generated about 500,000 dead tuples over 5 minutes. + # To keep replication lag from ballooning, we'll aim for 50,000 updates over 5 minutes. + # + # Assuming that there are 5 million rows affected (which is more than on + # GitLab.com), and that each batch of 50,000 rows takes up to 5 minutes, then + # we can migrate all the rows in 8.5 hours. + def up + MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation, index| + range = relation.pluck('MIN(id)', 'MAX(id)').first + + BackgroundMigrationWorker.perform_in(index * 5.minutes, MIGRATION, range) + end + end +end diff --git a/db/post_migrate/20171114104051_remove_empty_fork_networks.rb b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb new file mode 100644 index 00000000000..2fe99a1b9c1 --- /dev/null +++ b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb @@ -0,0 +1,36 @@ +class RemoveEmptyForkNetworks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 10_000 + + class MigrationForkNetwork < ActiveRecord::Base + include EachBatch + + self.table_name = 'fork_networks' + end + + class MigrationForkNetworkMembers < ActiveRecord::Base + self.table_name = 'fork_network_members' + end + + disable_ddl_transaction! + + def up + say 'Deleting empty ForkNetworks in batches' + + has_members = MigrationForkNetworkMembers + .where('fork_network_members.fork_network_id = fork_networks.id') + .select(1) + MigrationForkNetwork.where('NOT EXISTS (?)', has_members) + .each_batch(of: BATCH_SIZE) do |networks| + deleted = networks.delete_all + + say "Deleted #{deleted} rows in batch" + end + end + + def down + # nothing + end +end diff --git a/db/schema.rb b/db/schema.rb index 37e08d453c8..a82270390f1 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: 20171106180641) do +ActiveRecord::Schema.define(version: 20171121144800) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -140,6 +140,15 @@ ActiveRecord::Schema.define(version: 20171106180641) do t.integer "circuitbreaker_storage_timeout", default: 30 t.integer "circuitbreaker_access_retries", default: 3 t.integer "circuitbreaker_backoff_threshold", default: 80 + t.boolean "throttle_unauthenticated_enabled", default: false, null: false + t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false + t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false + t.boolean "throttle_authenticated_api_enabled", default: false, null: false + t.integer "throttle_authenticated_api_requests_per_period", default: 7200, null: false + t.integer "throttle_authenticated_api_period_in_seconds", default: 3600, null: false + t.boolean "throttle_authenticated_web_enabled", default: false, null: false + t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false + t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false end create_table "audit_events", force: :cascade do |t| @@ -373,7 +382,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree - add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree + add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree @@ -1031,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do create_table "merge_requests", force: :cascade do |t| t.string "target_branch", null: false t.string "source_branch", null: false - t.integer "source_project_id", null: false + t.integer "source_project_id" t.integer "author_id" t.integer "assignee_id" t.string "title" @@ -1071,6 +1080,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree + add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree @@ -1079,6 +1089,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} + add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree create_table "merge_requests_closing_issues", force: :cascade do |t| t.integer "merge_request_id", null: false @@ -1956,7 +1967,13 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify + add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify + add_foreign_key "merge_requests", "projects", column: "source_project_id", name: "fk_3308fe130c", on_delete: :nullify add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade + add_foreign_key "merge_requests", "users", column: "assignee_id", name: "fk_6149611a04", on_delete: :nullify + add_foreign_key "merge_requests", "users", column: "author_id", name: "fk_e719a85f8a", on_delete: :nullify + add_foreign_key "merge_requests", "users", column: "merge_user_id", name: "fk_ad525e1f87", on_delete: :nullify + add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 4d3be0ab8f6..a88e67bfeb5 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -53,7 +53,9 @@ or in different cloud availability zones. > **Note:** GitLab recommends against choosing this HA method because of the complexity of managing DRBD and crafting automatic failover. This is - *compatible* with GitLab, but not officially *supported*. + *compatible* with GitLab, but not officially *supported*. If you are + an EE customer, support will help you with GitLab related problems, but if the + root cause is identified as DRBD, we will not troubleshoot further. Components/Servers Required: 2 servers/virtual machines (one active/one passive) diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index be538ea250a..83a714810c1 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -163,6 +163,34 @@ separate Rails process to debug the issue: 1. In a new window, run `top`. It should show this ruby process using 100% CPU. Write down the PID. 1. Follow step 2 from the previous section on using gdb. +### GitLab: API is not accessible + +This often occurs when gitlab-shell attempts to request authorization via the +internal API (e.g., `http://localhost:8080/api/v4/internal/allowed`), and +something in the check fails. There are many reasons why this may happen: + +1. Timeout connecting to a database (e.g., PostgreSQL or Redis) +1. Error in Git hooks or push rules +1. Error accessing the repository (e.g., stale NFS handles) + +To diagnose this problem, try to reproduce the problem and then see if there +is a unicorn worker that is spinning via `top`. Try to use the `gdb` +techniques above. In addition, using `strace` may help isolate issues: + +```shell +strace -tt -T -f -s 1024 -p <PID of unicorn worker> -o /tmp/unicorn.txt +``` + +If you cannot isolate which Unicorn worker is the issue, try to run `strace` +on all the Unicorn workers to see where the `/internal/allowed` endpoint gets +stuck: + +```shell +ps auwx | grep unicorn | awk '{ print " -p " $2}' | xargs strace -tt -T -f -s 1024 -o /tmp/unicorn.txt +``` + +The output in `/tmp/unicorn.txt` may help diagnose the root cause. + # More information * [Debugging Stuck Ruby Processes](https://blog.newrelic.com/2013/04/29/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9/) diff --git a/doc/api/groups.md b/doc/api/groups.md index 6a6e94195a7..c1b5737c247 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -82,6 +82,8 @@ GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_valu ## List a groups's subgroups +> [Introduced][ce-15142] in GitLab 10.3. + Get a list of visible direct subgroups in this group. When accessed without authentication, only public groups are returned. @@ -513,3 +515,5 @@ And to switch pages add: ``` /groups?per_page=100&page=2 ``` + +[ce-15142]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15142 diff --git a/doc/api/settings.md b/doc/api/settings.md index 4e24e4bbfc3..b27220f57f4 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -79,7 +79,7 @@ PUT /application/settings | `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts | -| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. | +| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and masters can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but masters can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. | | `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | diff --git a/doc/api/users.md b/doc/api/users.md index aa711090af1..478d747a50d 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -297,6 +297,7 @@ Parameters: - `location` (optional) - User's location - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false +- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default) - `external` (optional) - Flags the user as external - true or false(default) - `avatar` (optional) - Image file for user's avatar diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md index c83d3f6f248..286f3dee665 100644 --- a/doc/ci/git_submodules.md +++ b/doc/ci/git_submodules.md @@ -8,7 +8,7 @@ with the use of [SSH keys](ssh_keys/README.md). - With GitLab 8.12 onward, your permissions are used to evaluate what a CI job can access. More information about how this system works can be found in the - [Jobs permissions model](../user/permissions.md#jobs-permissions). + [Jobs permissions model](../user/permissions.md#job-permissions). - The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance. ## Configuring the `.gitmodules` file diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 6ad70707594..f40d2c5e347 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -124,7 +124,7 @@ stages: 1. First, all jobs of `build` are executed in parallel. 1. If all jobs of `build` succeed, the `test` jobs are executed in parallel. 1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel. -1. If all jobs of `deploy` succeed, the commit is marked as `success`. +1. If all jobs of `deploy` succeed, the commit is marked as `passed`. 1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. diff --git a/doc/development/README.md b/doc/development/README.md index 9b43170236f..6892838be7f 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -71,6 +71,7 @@ comments: false - [Iterating tables in batches](iterating_tables_in_batches.md) - [Ordering table columns](ordering_table_columns.md) - [Verifying database capabilities](verifying_database_capabilities.md) +- [Database Debugging and Troubleshooting](database_debugging.md) ## Testing guides diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md new file mode 100644 index 00000000000..50eb8005b44 --- /dev/null +++ b/doc/development/database_debugging.md @@ -0,0 +1,55 @@ +# Database Debugging and Troubleshooting + +This section is to help give some copy-pasta you can use as a reference when you +run into some head-banging database problems. + +An easy first step is to search for your error in Slack or google "GitLab <my error>". + +--- + +Available `RAILS_ENV` + + - `production` (generally not for your main GDK db, but you may need this for e.g. omnibus) + - `development` (this is your main GDK db) + - `test` (used for tests like rspec and spinach) + + +## Nuke everything and start over + +If you just want to delete everything and start over with an empty DB (~1 minute): + + - `bundle exec rake db:reset RAILS_ENV=development` + +If you just want to delete everything and start over with dummy data (~40 minutes). This also does `db:reset` and runs DB-specific migrations: + + - `bundle exec rake dev:setup RAILS_ENV=development` + +If your test DB is giving you problems, it is safe to nuke it because it doesn't contain important data: + + - `bundle exec rake db:reset RAILS_ENV=test` + +## Migration wrangling + + - `bundle exec rake db:migrate RAILS_ENV=development`: Execute any pending migrations that you may have picked up from a MR + - `bundle exec rake db:migrate:status RAILS_ENV=development`: Check if all migrations are `up` or `down` + - `bundle exec rake db:migrate:down VERSION=20170926203418 RAILS_ENV=development`: Tear down a migration + - `bundle exec rake db:migrate:up VERSION=20170926203418 RAILS_ENV=development`: Setup a migration + - `bundle exec rake db:migrate:redo VERSION=20170926203418 RAILS_ENV=development`: Re-run a specific migration + + +## Manually access the database + +Access the database via one of these commands (they all get you to the same place) + +``` +gdk psql -d gitlabhq_development +bundle exec rails dbconsole RAILS_ENV=development +bundle exec rails db RAILS_ENV=development +``` + + - `\q`: Quit/exit + - `\dt`: List all tables + - `\d+ issues`: List columns for `issues` table + - `CREATE TABLE board_labels();`: Create a table called `board_labels` + - `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run + - `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md new file mode 100644 index 00000000000..962fe3dcec9 --- /dev/null +++ b/doc/development/fe_guide/axios.md @@ -0,0 +1,68 @@ +# Axios +We use [axios][axios] to communicate with the server in Vue applications and most new code. + +In order to guarantee all defaults are set you *should not use `axios` directly*, you should import `axios` from `axios_utils`. + +## CSRF token +All our request require a CSRF token. +To guarantee this token is set, we are importing [axios][axios], setting the token, and exporting `axios` . + +This exported module should be used instead of directly using `axios` to ensure the token is set. + +## Usage +```javascript + import axios from '~/lib/utils/axios_utils'; + + axios.get(url) + .then((response) => { + // `data` is the response that was provided by the server + const data = response.data; + + // `headers` the headers that the server responded with + // All header names are lower cased + const paginationData = response.headers; + }) + .catch(() => { + //handle the error + }); +``` + +## Mock axios response on tests + +To help us mock the responses we need we use [axios-mock-adapter][axios-mock-adapter] + + +```javascript + import axios from '~/lib/utils/axios_utils'; + import MockAdapter from 'axios-mock-adapter'; + + let mock; + beforeEach(() => { + // This sets the mock adapter on the default instance + mock = new MockAdapter(axios); + // Mock any GET request to /users + // arguments for reply are (status, data, headers) + mock.onGet('/users').reply(200, { + users: [ + { id: 1, name: 'John Smith' } + ] + }); + }); + + afterEach(() => { + mock.reset(); + }); +``` + +### Mock poll requests on tests with axios + +Because polling function requires an header object, we need to always include an object as the third argument: + +```javascript + mock.onGet('/users').reply(200, { foo: 'bar' }, {}); +``` + +[axios]: https://github.com/axios/axios +[axios-instance]: #creating-an-instance +[axios-interceptors]: https://github.com/axios/axios#interceptors +[axios-mock-adapter]: https://github.com/ctimmerm/axios-mock-adapter diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md new file mode 100644 index 00000000000..e1660ac5caa --- /dev/null +++ b/doc/development/fe_guide/dropdowns.md @@ -0,0 +1,38 @@ +# Dropdowns + + +## How to style a bootstrap dropdown +1. Use the HTML structure provided by the [docs][bootstrap-dropdowns] +1. Add a specific class to the top level `.dropdown` element + + + ```Haml + .dropdown.my-dropdown + %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false } + %span.dropdown-toggle-text + Toggle Dropdown + = icon('chevron-down') + + %ul.dropdown-menu + %li + %a + item! + ``` + + Or use the helpers + ```Haml + .dropdown.my-dropdown + = dropdown_toggle('Toogle!', { toggle: 'dropdown' }) + = dropdown_content + %li + %a + item! + ``` + +1. Include the mixin in CSS + + ```SCSS + @include new-style-dropdown('.my-dropdown '); + ``` + +[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index cef62618a3c..b288ee95722 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on ### Usage in HAML/Rails -To use a sprite Icon in HAML or Rails we use a specific helper function : +To use a sprite Icon in HAML or Rails we use a specific helper function : `sprite_icon(icon_name, size: nil, css_class: '')` -**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`). +**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`). + **size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class) + **css_class (optional)** If you want to add additional css classes -**Example** +**Example** `= sprite_icon('issues', size: 72, css_class: 'icon-danger')` @@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function : `<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>` +### Usage in Vue + +We have a special Vue component for our sprite icons in `\vue_shared\components\icon.vue`. + +Sample usage : + +`<icon + name="retry" + :size="32" + css-classes="top" + />` + +**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`). + +**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes) + +**css-classes (optional)** Additional CSS Classes to add to the svg tag. + ### Usage in HTML/JS -Please use the following function inside JS to render an icon : +Please use the following function inside JS to render an icon : `gl.utils.spriteIcon(iconName)` ## Adding a new icon to the sprite All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. -To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. +To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced. # SVG Illustrations diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 8f956681693..72cb557d054 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -71,12 +71,14 @@ Vue specific design patterns and practices. --- -## [Vue Resource](vue_resource.md) -Vue resource specific practices and gotchas. +## [Axios](axios.md) +Axios specific practices and gotchas. ## [Icons](icons.md) How we use SVG for our Icons. +## [Dropdowns](dropdowns.md) +How we use dropdowns. --- ## Style Guides diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index f88f0753687..6e9f18dd1c3 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -178,16 +178,13 @@ itself, please read this guide: [State Management][state-management] The Service is a class used only to communicate with the server. It does not store or manipulate any data. It is not aware of the store or the components. -We use [vue-resource][vue-resource-repo] to communicate with the server. -Refer to [vue resource](vue_resource.md) for more details. +We use [axios][axios] to communicate with the server. +Refer to [axios](axios.md) for more details. -Vue Resource should only be imported in the service file. +Axios instance should only be imported in the service file. ```javascript - import Vue from 'vue'; - import VueResource from 'vue-resource'; - - Vue.use(VueResource); + import axios from 'javascripts/lib/utils/axios_utils'; ``` ### End Result @@ -230,15 +227,14 @@ export default class Store { } // service.js -import Vue from 'vue'; -import VueResource from 'vue-resource'; -import 'vue_shared/vue_resource_interceptor'; - -Vue.use(VueResource); +import axios from 'javascripts/lib/utils/axios_utils' export default class Service { constructor(options) { - this.todos = Vue.resource(endpoint.todosEndpoint); + this.todos = axios.create({ + baseURL: endpoint.todosEndpoint + }); + } getTodos() { @@ -477,50 +473,8 @@ The main return value of a Vue component is the rendered output. In order to tes need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that: ### Stubbing API responses -[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with -the response we need: - - ```javascript - // Mock the service to return data - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify([{ - title: 'This is a todo', - body: 'This is the text' - }]), { - status: 200, - })); - }; +Refer to [mock axios](axios.md#mock-axios-response-on-tests) - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); - - it('should do something', (done) => { - setTimeout(() => { - // Test received data - done(); - }, 0); - }); - ``` - -1. Headers interceptor -Refer to [this section](vue.md#headers) - -1. Use `$.mount()` to mount the component - -```javascript -// bad -new Component({ - el: document.createElement('div') -}); - -// good -new Component().$mount(); -``` ## Vuex To manage the state of an application you may use [Vuex][vuex-docs]. @@ -721,7 +675,6 @@ describe('component', () => { [component-system]: https://vuejs.org/v2/guide/#Composing-with-Components [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch [one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow -[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors [vue-test]: https://vuejs.org/v2/guide/unit-testing.html [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [flux]: https://facebook.github.io/flux @@ -729,3 +682,6 @@ describe('component', () => { [vuex-structure]: https://vuex.vuejs.org/en/structure.html [vuex-mutations]: https://vuex.vuejs.org/en/mutations.html [vuex-testing]: https://vuex.vuejs.org/en/testing.html +[axios]: https://github.com/axios/axios +[axios-interceptors]: https://github.com/axios/axios#interceptors + diff --git a/doc/development/fe_guide/vue_resource.md b/doc/development/fe_guide/vue_resource.md deleted file mode 100644 index c376c5c32bf..00000000000 --- a/doc/development/fe_guide/vue_resource.md +++ /dev/null @@ -1,72 +0,0 @@ -# Vue Resouce -In Vue applications we use [vue-resource][vue-resource-repo] to communicate with the server. - -## HTTP Status Codes - -### `.json()` -When making a request to the server, you will most likely need to access the body of the response. -Use `.json()` to convert. Because `.json()` returns a Promise the follwoing structure should be used: - - ```javascript - service.get('url') - .then(resp => resp.json()) - .then((data) => { - this.store.storeData(data); - }) - .catch(() => new Flash('Something went wrong')); - ``` - - -When using `Poll` (`app/assets/javascripts/lib/utils/poll.js`), the `successCallback` needs to handle `.json()` as a Promise: - ```javascript - successCallback: (response) => { - return response.json().then((data) => { - // handle the response - }); - } - ``` - -### 204 -Some endpoints - usually `delete` endpoints - return `204` as the success response. -When handling `204 - No Content` responses, we cannot use `.json()` since it tries to parse the non-existant body content. - -When handling `204` responses, do not use `.json`, otherwise the promise will throw an error and will enter the `catch` statement: - -```javascript - Vue.http.delete('path') - .then(() => { - // success! - }) - .catch(() => { - // handle error - }) -``` - -## Headers -Headers are being parsed into a plain object in an interceptor. -In Vue-resource 1.x `headers` object was changed into an `Headers` object. In order to not change all old code, an interceptor was added. - -If you need to write a unit test that takes the headers in consideration, you need to include an interceptor to parse the headers after your test interceptor. -You can see an example in `spec/javascripts/environments/environment_spec.js`: - ```javascript - import { headersInterceptor } from './helpers/vue_resource_helper'; - - beforeEach(() => { - Vue.http.interceptors.push(myInterceptor); - Vue.http.interceptors.push(headersInterceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, myInterceptor); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }); - ``` - -## CSRF token -We use a Vue Resource interceptor to manage the CSRF token. -`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors. -Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js` -since it's already being loaded by `common_vue.js`. - - -[vue-resource-repo]: https://github.com/pagekit/vue-resource diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 9b8ab5da74e..a235dd74909 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -198,7 +198,43 @@ end Keep in mind that this operation can easily take 10-15 minutes to complete on larger installations (e.g. GitLab.com). As a result you should only add default -values if absolutely necessary. +values if absolutely necessary. There is a RuboCop cop that will fail if this +method is used on some tables that are very large on GitLab.com, which would +cause other issues. + +## Updating an existing column + +To update an existing column to a particular value, you can use +`update_column_in_batches` (`add_column_with_default` uses this internally to +fill in the default value). This will split the updates into batches, so we +don't update too many rows at in a single statement. + +This updates the column `foo` in the `projects` table to 10, where `some_column` +is `'hello'`: + +```ruby +update_column_in_batches(:projects, :foo, 10) do |table, query| + query.where(table[:some_column].eq('hello')) +end +``` + +To perform a computed update, the value can be wrapped in `Arel.sql`, so Arel +treats it as an SQL literal. The below example is the same as the one above, but +the value is set to the product of the `bar` and `baz` columns: + +```ruby +update_value = Arel.sql('bar * baz') + +update_column_in_batches(:projects, :foo, update_value) do |table, query| + query.where(table[:some_column].eq('hello')) +end +``` + +Like `add_column_with_default`, there is a RuboCop cop to detect usage of this +on large tables. In the case of `update_column_in_batches`, it may be acceptable +to run on a large table, as long as it is only updating a small subset of the +rows in the table, but do not ignore that without validating on the GitLab.com +staging environment - or asking someone else to do so for you - beforehand. ## Integer column type diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md index e0127aaed4c..12e90101139 100644 --- a/doc/development/query_recorder.md +++ b/doc/development/query_recorder.md @@ -22,6 +22,52 @@ As an example you might create 5 issues in between counts, which would cause the > **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible. +## Finding the source of the query + +It may be useful to identify the source of the queries by looking at the call backtrace. +To enable this, run the specs with the `QUERY_RECORDER_DEBUG` environment variable set. For example: + +``` +QUERY_RECORDER_DEBUG=1 bundle exec rspec spec/requests/api/projects_spec.rb +``` + +This will log calls to QueryRecorder into the `test.log`. For example: + +``` +QueryRecorder SQL: SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened')) AND "issues"."confidential" = $2 + --> /home/user/gitlab/gdk/gitlab/spec/support/query_recorder.rb:19:in `callback' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:127:in `finish' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `block in finish' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `each' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `finish' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:36:in `finish' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:25:in `instrument' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:478:in `log' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:601:in `exec_cache' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:585:in `execute_and_clear' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:356:in `select' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `block in select_all' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:83:in `cache_sql' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `select_all' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:270:in `execute_simple_calculation' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:227:in `perform_calculation' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:133:in `calculate' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:48:in `count' + --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:20:in `uncached_count' + --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `block in count' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `block in fetch' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:585:in `block in save_block_result_to_cache' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `block in instrument' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications.rb:166:in `instrument' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `instrument' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:584:in `save_block_result_to_cache' + --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `fetch' + --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `count' + --> /home/user/gitlab/gdk/gitlab/app/models/project.rb:1296:in `open_issues_count' +``` + ## See also - [Bullet](profiling.md#Bullet) For finding `N+1` query problems diff --git a/doc/install/installation.md b/doc/install/installation.md index 4efe911b778..88000f4c7a9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -513,8 +513,7 @@ Check if GitLab and its environment are configured correctly: ### 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 + sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production ### Compile Assets diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 7bf126eec5d..baecf9455b0 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -80,13 +80,13 @@ errors during usage. - 256GB RAM supports up to 32,000 users - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) -We recommend having at least 2GB of swap on your server, even if you currently have +We recommend having at least [2GB of swap on your server](https://askubuntu.com/a/505344/310789), even if you currently have enough available RAM. Having swap will help reduce the chance of errors occurring if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916) to a low value like `10` to make the most of your RAM while still having the swap available when needed. -Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. +Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. ## Database @@ -146,7 +146,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal. For all machines that have 2GB and up we recommend a minimum of three unicorn workers. If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. -To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). +To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). ## Redis and Sidekiq diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 372e1909330..075feaeead9 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -22,6 +22,7 @@ Visit the links below for details: - [Redmine](../user/project/integrations/redmine.md) - [Jira](../user/project/integrations/jira.md) - [Bugzilla](../user/project/integrations/bugzilla.md) +- [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md) ### Service Template diff --git a/doc/user/discussions/img/image_resolved_discussion.png b/doc/user/discussions/img/image_resolved_discussion.png Binary files differindex ed00b5c77fe..ed00b5c77fe 100755..100644 --- a/doc/user/discussions/img/image_resolved_discussion.png +++ b/doc/user/discussions/img/image_resolved_discussion.png diff --git a/doc/user/discussions/img/onion_skin_view.png b/doc/user/discussions/img/onion_skin_view.png Binary files differindex 91c3b396844..91c3b396844 100755..100644 --- a/doc/user/discussions/img/onion_skin_view.png +++ b/doc/user/discussions/img/onion_skin_view.png diff --git a/doc/user/discussions/img/swipe_view.png b/doc/user/discussions/img/swipe_view.png Binary files differindex 82d6e52173c..82d6e52173c 100755..100644 --- a/doc/user/discussions/img/swipe_view.png +++ b/doc/user/discussions/img/swipe_view.png diff --git a/doc/user/discussions/img/two_up_view.png b/doc/user/discussions/img/two_up_view.png Binary files differindex d9e90708e87..d9e90708e87 100755..100644 --- a/doc/user/discussions/img/two_up_view.png +++ b/doc/user/discussions/img/two_up_view.png diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 454988b9b80..fb61e360996 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -368,6 +368,37 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ >**Note:** This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual]. +### Mermaid + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid + +It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid]. + +In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block. + +Example: + + ```mermaid + graph TD; + A-->B; + A-->C; + B-->D; + C-->D; + ``` + +Becomes: + +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` + +For details see the [Mermaid official page][mermaid]. + ## Standard Markdown ### Headers @@ -814,6 +845,7 @@ A link starting with a `/` is relative to the wiki root. [^2]: This is my awesome footnote. [markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md +[mermaid]: https://mermaidjs.github.io/ "Mermaid website" [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [katex]: https://github.com/Khan/KaTeX "KaTeX website" diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index f2ad42f21fd..022d6317555 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -55,9 +55,10 @@ You have 6 options here that you can use for your default dashboard view: The project home page content setting allows you to choose what content you want to see on a project’s home page. -You can choose between 2 options: +You can choose between 3 options: - Show the files and the readme (default) +- Show the readme - Show the project’s activity [rouge]: http://rouge.jneen.net/ "Rouge website" diff --git a/doc/user/project/clusters/img/cluster-applications.png b/doc/user/project/clusters/img/cluster-applications.png Binary files differdeleted file mode 100644 index 7c82d19297e..00000000000 --- a/doc/user/project/clusters/img/cluster-applications.png +++ /dev/null diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 27b4b49c207..cf0c7c109a8 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -1,14 +1,15 @@ -# Connecting GitLab with GKE +# Connecting GitLab with a Kubernetes cluster > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1. CAUTION: **Warning:** The Cluster integration is currently in **Beta**. -Connect your project to Google Container Engine (GKE) in a few steps. - With a cluster associated to your project, you can use Review Apps, deploy your -applications, run your pipelines, and much more in an easy way. +applications, run your pipelines, and much more, in an easy way. + +Connect your project to Google Kubernetes Engine (GKE) or your own Kubernetes +cluster in a few steps. NOTE: **Note:** The Cluster integration will eventually supersede the @@ -30,36 +31,58 @@ prerequisites must be met: - You must have Master [permissions] in order to be able to access the **Cluster** page. -If all of the above requirements are met, you can proceed to add a new cluster. +If all of the above requirements are met, you can proceed to add a new GKE +cluster. ## Adding a cluster NOTE: **Note:** You need Master [permissions] and above to add a cluster. +There are two options when adding a new cluster; either use Google Kubernetes +Engine (GKE) or provide the credentials to your own Kubernetes cluster. + To add a new cluster: -1. Navigate to your project's **CI/CD > Cluster** page. -1. Connect your Google account if you haven't done already by clicking the - "Sign-in with Google" button. -1. Fill in the requested values: - - **Cluster name** (required) - The name you wish to give the cluster. - - **GCP project ID** (required) - The ID of the project you created in your GCP - console that will host the Kubernetes cluster. This must **not** be confused - with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). - - **Zone** - The zone under which the cluster will be created. Read more about - [the available zones](https://cloud.google.com/compute/docs/regions-zones/). - - **Number of nodes** - The number of nodes you wish the cluster to have. - - **Machine type** - The machine type of the Virtual Machine instance that - the cluster will be based on. Read more about [the available machine types](https://cloud.google.com/compute/docs/machine-types). - - **Project namespace** - The unique namespace for this project. By default you - don't have to fill it in; by leaving it blank, GitLab will create one for you. -1. Click the **Create cluster** button. - -After a few moments your cluster should be created. If something goes wrong, +1. Navigate to your project's **CI/CD > Cluster** page +1. If you want to let GitLab create a cluster on GKE for you, go through the + following steps, otherwise skip to the next one. + 1. Click on **Create with GKE** + 1. Connect your Google account if you haven't done already by clicking the + **Sign in with Google** button + 1. Fill in the requested values: + - **Cluster name** (required) - The name you wish to give the cluster. + - **GCP project ID** (required) - The ID of the project you created in your GCP + console that will host the Kubernetes cluster. This must **not** be confused + with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). + - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) + under which the cluster will be created. + - **Number of nodes** - The number of nodes you wish the cluster to have. + - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) + of the Virtual Machine instance that the cluster will be based on. + - **Project namespace** - The unique namespace for this project. By default you + don't have to fill it in; by leaving it blank, GitLab will create one for you. +1. If you want to use your own existing Kubernetes cluster, click on + **Add an existing cluster** and fill in the details as described in the + [Kubernetes integration](../integrations/kubernetes.md) documentation. +1. Finally, click the **Create cluster** button + +After a few moments, your cluster should be created. If something goes wrong, you will be notified. -Now, you can proceed to [enable the Cluster integration](#enabling-or-disabling-the-cluster-integration). +You can now proceed to install some pre-defined applications and then +enable the Cluster integration. + +## Installing applications + +GitLab provides a one-click install for various applications which will be +added directly to your configured cluster. Those applications are needed for +[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). + +| Application | GitLab version | Description | +| ----------- | :------------: | ----------- | +| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | +| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | ## Enabling or disabling the Cluster integration @@ -88,12 +111,3 @@ To remove the Cluster integration from your project, simply click on the and [add a cluster](#adding-a-cluster) again. [permissions]: ../../permissions.md - -## Installing applications - -GitLab provides a one-click install for -[Helm Tiller](https://docs.helm.sh/) and -[Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) -which will be added directly to your configured cluster. - -![Cluster application settings](img/cluster-applications.png) diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md new file mode 100644 index 00000000000..757522c2ae3 --- /dev/null +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -0,0 +1,20 @@ +# Custom Issue Tracker Service + +To enable the Custom Issue Tracker integration in a project, navigate to the +[Integrations page](project_services.md#accessing-the-project-services), click +the **Customer Issue Tracker** service, and fill in the required details on the page as described +in the table below. + +| Field | Description | +| ----- | ----------- | +| `title` | A title for the issue tracker (to differentiate between instances, for example) | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | Currently unused. Will be changed in a future release. | +| `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. | +| `new_issue_url` | Currently unused. Will be changed in a future release. | + + +## Referencing issues + +Issues are referenced with `#<ID>`, where `<ID>` is a number (example `#143`). +So with the example above, `#143` would refer to `https://customissuetracker.com/project-name/143`.
\ No newline at end of file diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 271adee7da1..17dcd152363 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -230,7 +230,7 @@ test: - docker run $CI_REGISTRY/group/other-project:latest ``` -[job permissions]: ../permissions.md#jobs-permissions +[job permissions]: ../permissions.md#job-permissions [comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302 [ext]: ../permissions.md#external-users [gitsub]: ../../ci/git_submodules.md diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 9ad15a12c3c..eac706be3a7 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -44,7 +44,7 @@ GitLab CI so that they can be used in your `.gitlab-ci.yml` file. To configure that a job can be executed only when the pipeline has been scheduled (or the opposite), you can use -[only and except](../../../ci/yaml/README.md#only-and-except) configuration keywords. +[only and except](../../../ci/yaml/README.md#only-and-except-simplified) configuration keywords. ``` job:on-schedule: diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 56f58fd755a..daa5463d680 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -115,10 +115,12 @@ pages. Depending on the status of your job, a badge can have the following values: +- pending - running -- success +- passed - failed - skipped +- canceled - unknown You can access a pipeline status badge image using the following link: diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c1c0d344917..9aeebc34525 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -6,9 +6,6 @@ module API module APIGuard extend ActiveSupport::Concern - PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze - PRIVATE_TOKEN_PARAM = :private_token - included do |base| # OAuth2 Resource Server Authentication use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| @@ -42,7 +39,7 @@ module API # Helper Methods for Grape Endpoint module HelperMethods - include Gitlab::Utils::StrongMemoize + include Gitlab::Auth::UserAuthFinders def find_current_user! user = find_user_from_access_token || find_user_from_warden @@ -53,76 +50,8 @@ module API user end - def access_token - strong_memoize(:access_token) do - find_oauth_access_token || find_personal_access_token - end - end - - def validate_access_token!(scopes: []) - return unless access_token - - case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) - when AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when AccessTokenValidationService::EXPIRED - raise ExpiredError - when AccessTokenValidationService::REVOKED - raise RevokedError - end - end - private - def find_user_from_access_token - return unless access_token - - validate_access_token! - - access_token.user || raise(UnauthorizedError) - end - - # Check the Rails session for valid authentication details - def find_user_from_warden - warden.try(:authenticate) if verified_request? - end - - def warden - env['warden'] - end - - # Check if the request is GET/HEAD, or if CSRF token is valid. - def verified_request? - Gitlab::RequestForgeryProtection.verified?(env) - end - - def find_oauth_access_token - token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) - return unless token - - # Expiration, revocation and scopes are verified in `find_user_by_access_token` - access_token = OauthAccessToken.by_token(token) - raise UnauthorizedError unless access_token - - access_token.revoke_previous_refresh_token! - access_token - end - - def find_personal_access_token - token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s - return unless token.present? - - # Expiration, revocation and scopes are verified in `find_user_by_access_token` - access_token = PersonalAccessToken.find_by(token: token) - raise UnauthorizedError unless access_token - - access_token - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - # An array of scopes that were registered (using `allow_access_with_scope`) # for the current endpoint class. It also returns scopes registered on # `API::API`, since these are meant to apply to all API routes. @@ -145,8 +74,11 @@ module API private def install_error_responders(base) - error_classes = [MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] + error_classes = [Gitlab::Auth::MissingTokenError, + Gitlab::Auth::TokenNotFoundError, + Gitlab::Auth::ExpiredError, + Gitlab::Auth::RevokedError, + Gitlab::Auth::InsufficientScopeError] base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend end @@ -155,25 +87,25 @@ module API proc do |e| response = case e - when MissingTokenError + when Gitlab::Auth::MissingTokenError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - when TokenNotFoundError + when Gitlab::Auth::TokenNotFoundError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( :invalid_token, "Bad Access Token.") - when ExpiredError + when Gitlab::Auth::ExpiredError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( :invalid_token, "Token is expired. You can either do re-authorization or token refresh.") - when RevokedError + when Gitlab::Auth::RevokedError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( :invalid_token, "Token was revoked. You have to re-authorize from the user.") - when InsufficientScopeError + when Gitlab::Auth::InsufficientScopeError # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) # does not include WWW-Authenticate header, which breaks the standard. Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( @@ -186,22 +118,5 @@ module API end end end - - # - # Exceptions - # - - MissingTokenError = Class.new(StandardError) - TokenNotFoundError = Class.new(StandardError) - ExpiredError = Class.new(StandardError) - RevokedError = Class.new(StandardError) - UnauthorizedError = Class.new(StandardError) - - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes.map { |s| s.try(:name) || s } - end - end end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2bc4039b019..38e05074353 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -180,10 +180,12 @@ module API if params[:path] commit.raw_diffs(limits: false).each do |diff| next unless diff.new_path == params[:path] + lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) lines.each do |line| next unless line.new_pos == params[:line] && line.type == params[:line_type] + break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3c8960cb1ab..b26c61ab8da 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -398,7 +398,7 @@ module API begin @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! } - rescue APIGuard::UnauthorizedError + rescue Gitlab::Auth::UnauthorizedError unauthorized! end end diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index 0a8f3073a50..dd4f6c41131 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -4,6 +4,7 @@ module API class Absence < Grape::Validations::Base def validate_param!(attr_name, params) return if params.respond_to?(:key?) && !params.key?(attr_name) + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 4c0db4d42b1..4b3c473b0bb 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -36,6 +36,18 @@ module API {} end + def fix_git_env_repository_paths(env, repository_path) + if obj_dir_relative = env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence + env['GIT_OBJECT_DIRECTORY'] = File.join(repository_path, obj_dir_relative) + end + + if alt_obj_dirs_relative = env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'].presence + env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_obj_dirs_relative.map { |dir| File.join(repository_path, dir) } + end + + env + end + def log_user_activity(actor) commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 282af32ca94..2cae53dba53 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -14,6 +14,7 @@ module API def get_runner_version_from_params return unless params['info'].present? + attributes_for_keys(%w(name version revision platform architecture), params['info']) end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 6e78ac2c903..451121a4cea 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -19,7 +19,9 @@ module API status 200 # Stores some Git-specific env thread-safely - Gitlab::Git::Env.set(parse_env) + env = parse_env + env = fix_git_env_repository_paths(env, repository_path) if project + Gitlab::Git::Env.set(env) actor = if params[:key_id] diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 0b9ab4eeb05..ceaaeca4046 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -33,7 +33,7 @@ module API # paginate() only works with a relation. This could lead to a # mismatch between the pagination headers info and the actual notes # array returned, but this is really a edge-case. - paginate(noteable.notes) + paginate(noteable.notes.with_metadata) .reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note else @@ -50,7 +50,7 @@ module API end get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do noteable = find_project_noteable(noteables_str, params[:noteable_id]) - note = noteable.notes.find(params[:note_id]) + note = noteable.notes.with_metadata.find(params[:note_id]) can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) if can_read_note diff --git a/lib/api/runners.rb b/lib/api/runners.rb index d3559ef71be..e816fcdd928 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -165,17 +165,20 @@ module API def authenticate_show_runner!(runner) return if runner.is_shared || current_user.admin? + forbidden!("No access granted") unless user_can_access_runner?(runner) end def authenticate_update_runner!(runner) return if current_user.admin? + forbidden!("Runner is shared") if runner.is_shared? forbidden!("No access granted") unless user_can_access_runner?(runner) end def authenticate_delete_runner!(runner) return if current_user.admin? + forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner associated with more than one project") if runner.projects.count > 1 forbidden!("No access granted") unless user_can_access_runner?(runner) @@ -185,6 +188,7 @@ module API forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner is locked") if runner.locked? return if current_user.admin? + forbidden!("No access granted") unless user_can_access_runner?(runner) end diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 00eb7c60f16..c736cc32021 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -95,6 +95,7 @@ module API put ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet attrs = declared_params(include_missing: false).merge(request: request, api: true) diff --git a/lib/api/users.rb b/lib/api/users.rb index d80b364bd09..0cd89b1bcf8 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -31,7 +31,6 @@ module API optional :location, type: String, desc: 'The location of the user' optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' - optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' optional :avatar, type: File, desc: 'Avatar image for user' all_or_none_of :extern_uid, :provider @@ -101,6 +100,7 @@ module API requires :email, type: String, desc: 'The email of the user' optional :password, type: String, desc: 'The password of the new user' optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' + optional :skip_confirmation, type: Boolean, desc: 'Flag indicating the account is confirmed' at_least_one_of :password, :reset_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' @@ -134,6 +134,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' optional :email, type: String, desc: 'The email of the user' optional :password, type: String, desc: 'The password of the new user' + optional :skip_reconfirmation, type: Boolean, desc: 'Flag indicating the account skips the confirmation by email' optional :name, type: String, desc: 'The name of the user' optional :username, type: String, desc: 'The username of the user' use :optional_attributes diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index be360fbfc0c..0ef26aa696a 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -169,10 +169,12 @@ module API if params[:path] commit.raw_diffs(limits: false).each do |diff| next unless diff.new_path == params[:path] + lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) lines.each do |line| next unless line.new_pos == params[:line] && line.type == params[:line_type] + break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) end diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb index faa265f3314..c6d9957d452 100644 --- a/lib/api/v3/runners.rb +++ b/lib/api/v3/runners.rb @@ -51,6 +51,7 @@ module API helpers do def authenticate_delete_runner!(runner) return if current_user.admin? + forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner associated with more than one project") if runner.projects.count > 1 forbidden!("No access granted") unless user_can_access_runner?(runner) diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb index 0762fc02d70..126ec72248e 100644 --- a/lib/api/v3/snippets.rb +++ b/lib/api/v3/snippets.rb @@ -91,6 +91,7 @@ module API put ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet attrs = declared_params(include_missing: false) @@ -113,6 +114,7 @@ module API delete ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet snippet.destroy no_content! diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb new file mode 100644 index 00000000000..b545b947a2c --- /dev/null +++ b/lib/banzai/filter/mermaid_filter.rb @@ -0,0 +1,20 @@ +module Banzai + module Filter + class MermaidFilter < HTML::Pipeline::Filter + def call + doc.css('pre[lang="mermaid"]').add_class('mermaid') + doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid') + + # The `<code></code>` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb + # We want to keep context and consistency, so we the blocks are added for all filters. + # Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859 + doc.css('pre[lang="mermaid"]').each do |pre| + document = pre.at('code') + document.replace(document.content) + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 7da565043d1..a79a0154846 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -14,23 +14,26 @@ module Banzai end def highlight_node(node) - language = node.attr('lang') code = node.text - css_classes = "code highlight" - lexer = lexer_for(language) - lang = lexer.tag - - begin - code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang) - - css_classes << " js-syntax-highlight #{lang}" - rescue - lang = nil - # Gracefully handle syntax highlighter bugs/errors to ensure - # users can still access an issue/comment/etc. + css_classes = 'code highlight js-syntax-highlight' + language = node.attr('lang') + + if use_rouge?(language) + lexer = lexer_for(language) + language = lexer.tag + + begin + code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: language) + css_classes << " #{language}" + rescue + # Gracefully handle syntax highlighter bugs/errors to ensure + # users can still access an issue/comment/etc. + + language = nil + end end - highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>) + highlighted = %(<pre class="#{css_classes}" lang="#{language}" v-pre="true"><code>#{code}</code></pre>) # Extracted to a method to measure it replace_parent_pre_element(node, highlighted) @@ -51,6 +54,10 @@ module Banzai # Replace the parent `pre` element with the entire highlighted block node.parent.replace(highlighted) end + + def use_rouge?(language) + %w(math mermaid plantuml).exclude?(language) + end end end end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index 9bb8ed913d8..ecb3affbba5 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -86,6 +86,7 @@ module Banzai def save_options return {} unless base_context[:xhtml] + { save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML } end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 3208abfc538..55874ad50a3 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -14,6 +14,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::MathFilter, + Filter::MermaidFilter, Filter::UploadLinkFilter, Filter::VideoLinkFilter, Filter::ImageLazyLoadFilter, diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb index fb2faae02bc..a19a05e8c0d 100644 --- a/lib/banzai/querying.rb +++ b/lib/banzai/querying.rb @@ -52,8 +52,10 @@ module Banzai children.each do |child| next if child.text.blank? + node = nodes.shift break unless node == child + filtered_nodes << node end end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index 4d336068861..8932d4f2905 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -31,6 +31,7 @@ module Banzai nodes.each do |node| if node.has_attribute?(group_attr) next unless can_read_group_reference?(node, user, groups) + visible << node elsif can_read_project_reference?(node) visible << node diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 5cb9adf52b0..0050295eeda 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -149,6 +149,7 @@ module Banzai def self.full_cache_key(cache_key, pipeline_name) return unless cache_key + ["banzai", *cache_key, pipeline_name || :full] end @@ -157,6 +158,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)) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb index ae65653645b..b1949d693ad 100644 --- a/lib/declarative_policy.rb +++ b/lib/declarative_policy.rb @@ -30,6 +30,7 @@ module DeclarativePolicy policy_class = class_for_class(subject.class) raise "no policy for #{subject.class.name}" if policy_class.nil? + policy_class end @@ -84,6 +85,7 @@ module DeclarativePolicy while subject.respond_to?(:declarative_policy_delegate) raise ArgumentError, "circular delegations" if seen.include?(subject.object_id) + seen << subject.object_id subject = subject.declarative_policy_delegate end diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb index b028169f500..47542194497 100644 --- a/lib/declarative_policy/base.rb +++ b/lib/declarative_policy/base.rb @@ -276,6 +276,7 @@ module DeclarativePolicy # boolean `false` def cache(key, &b) return @cache[key] if cached?(key) + @cache[key] = yield end @@ -291,6 +292,7 @@ module DeclarativePolicy @_conditions[name] ||= begin raise "invalid condition #{name}" unless self.class.conditions.key?(name) + ManifestCondition.new(self.class.conditions[name], self) end end diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb index 0804edba016..780d8f707bd 100644 --- a/lib/declarative_policy/cache.rb +++ b/lib/declarative_policy/cache.rb @@ -3,6 +3,7 @@ module DeclarativePolicy class << self def user_key(user) return '<anonymous>' if user.nil? + id_for(user) end @@ -15,6 +16,7 @@ module DeclarativePolicy def subject_key(subject) return '<nil>' if subject.nil? return subject.inspect if subject.is_a?(Symbol) + "#{subject.class.name}:#{id_for(subject)}" end diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb index 7cfa82a9a9f..e309244a3b3 100644 --- a/lib/declarative_policy/rule.rb +++ b/lib/declarative_policy/rule.rb @@ -83,6 +83,7 @@ module DeclarativePolicy def cached_pass?(context) condition = context.condition(@name) return nil unless condition.cached? + condition.pass? end @@ -109,6 +110,7 @@ module DeclarativePolicy def delegated_context(context) policy = context.delegated_policies[@delegate_name] raise MissingDelegate if policy.nil? + policy end @@ -121,6 +123,7 @@ module DeclarativePolicy def cached_pass?(context) condition = delegated_context(context).condition(@name) return nil unless condition.cached? + condition.pass? rescue MissingDelegate false @@ -157,6 +160,7 @@ module DeclarativePolicy def cached_pass?(context) runner = context.runner(@ability) return nil unless runner.cached? + runner.pass? end @@ -258,6 +262,7 @@ module DeclarativePolicy def score(context) return 0 unless cached_pass?(context).nil? + @rules.map { |r| r.score(context) }.inject(0, :+) end diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb index 45ff2ef9ced..77c91817382 100644 --- a/lib/declarative_policy/runner.rb +++ b/lib/declarative_policy/runner.rb @@ -43,6 +43,7 @@ module DeclarativePolicy # used by Rule::Ability. See #steps_by_score def score return 0 if cached? + steps.map(&:score).inject(0, :+) end diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index de391de9059..69d981e8be9 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -8,6 +8,7 @@ class FileSizeValidator < ActiveModel::EachValidator def initialize(options) if range = (options.delete(:in) || options.delete(:within)) raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range) + options[:minimum], options[:maximum] = range.begin, range.end options[:maximum] -= 1 if range.exclude_end? end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index b4012ebbb99..7127948cf00 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -58,9 +58,9 @@ module Gitlab def protection_options { "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, - "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE, - "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH, - "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL + "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Masters can push to the branch." => PROTECTION_DEV_CAN_MERGE, + "Partially protected: Both developers and masters can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH, + "Fully protected: Developers cannot push new commits, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL } end diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb new file mode 100644 index 00000000000..46ec040ce92 --- /dev/null +++ b/lib/gitlab/auth/request_authenticator.rb @@ -0,0 +1,25 @@ +# Use for authentication only, in particular for Rack::Attack. +# Does not perform authorization of scopes, etc. +module Gitlab + module Auth + class RequestAuthenticator + include UserAuthFinders + + attr_reader :request + + def initialize(request) + @request = request + end + + def user + find_sessionless_user || find_user_from_warden + end + + def find_sessionless_user + find_user_from_access_token || find_user_from_rss_token + rescue Gitlab::Auth::AuthenticationError + nil + end + end + end +end diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb new file mode 100644 index 00000000000..b4114a3ac96 --- /dev/null +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -0,0 +1,109 @@ +module Gitlab + module Auth + # + # Exceptions + # + + AuthenticationError = Class.new(StandardError) + MissingTokenError = Class.new(AuthenticationError) + TokenNotFoundError = Class.new(AuthenticationError) + ExpiredError = Class.new(AuthenticationError) + RevokedError = Class.new(AuthenticationError) + UnauthorizedError = Class.new(AuthenticationError) + + class InsufficientScopeError < AuthenticationError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes.map { |s| s.try(:name) || s } + end + end + + module UserAuthFinders + include Gitlab::Utils::StrongMemoize + + PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze + PRIVATE_TOKEN_PARAM = :private_token + + # Check the Rails session for valid authentication details + def find_user_from_warden + current_request.env['warden']&.authenticate if verified_request? + end + + def find_user_from_rss_token + return unless current_request.path.ends_with?('.atom') || current_request.format.atom? + + token = current_request.params[:rss_token].presence + return unless token + + User.find_by_rss_token(token) || raise(UnauthorizedError) + end + + def find_user_from_access_token + return unless access_token + + validate_access_token! + + access_token.user || raise(UnauthorizedError) + end + + def validate_access_token!(scopes: []) + return unless access_token + + case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when AccessTokenValidationService::EXPIRED + raise ExpiredError + when AccessTokenValidationService::REVOKED + raise RevokedError + end + end + + private + + def access_token + strong_memoize(:access_token) do + find_oauth_access_token || find_personal_access_token + end + end + + def find_personal_access_token + token = + current_request.params[PRIVATE_TOKEN_PARAM].presence || + current_request.env[PRIVATE_TOKEN_HEADER].presence + + return unless token + + # Expiration, revocation and scopes are verified in `validate_access_token!` + PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError) + end + + def find_oauth_access_token + token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods) + return unless token + + # Expiration, revocation and scopes are verified in `validate_access_token!` + oauth_token = OauthAccessToken.by_token(token) + raise UnauthorizedError unless oauth_token + + oauth_token.revoke_previous_refresh_token! + oauth_token + end + + # Check if the request is GET/HEAD, or if CSRF token is valid. + def verified_request? + Gitlab::RequestForgeryProtection.verified?(current_request.env) + end + + def ensure_action_dispatch_request(request) + return request if request.is_a?(ActionDispatch::Request) + + ActionDispatch::Request.new(request.env) + end + + def current_request + @current_request ||= ensure_action_dispatch_request(request) + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb new file mode 100644 index 00000000000..7e109e96e73 --- /dev/null +++ b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb @@ -0,0 +1,30 @@ +module Gitlab + module BackgroundMigration + class PopulateMergeRequestsLatestMergeRequestDiffId + BATCH_SIZE = 1_000 + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include ::EachBatch + end + + def perform(start_id, stop_id) + update = ' + latest_merge_request_diff_id = ( + SELECT MAX(id) + FROM merge_request_diffs + WHERE merge_requests.id = merge_request_diffs.merge_request_id + )'.squish + + MergeRequest + .where(id: start_id..stop_id) + .where(latest_merge_request_diff_id: nil) + .each_batch(of: BATCH_SIZE) do |relation| + + relation.update_all(update) + end + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 033ecd15749..d48ae17aeaf 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -61,9 +61,9 @@ module Gitlab def import_wiki return if project.wiki.repository_exists? - path_with_namespace = "#{project.full_path}.wiki" + disk_path = project.wiki.disk_path import_url = project.import_url.sub(/\.git\z/, ".git/wiki") - gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url) + gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url) rescue StandardError => e errors << { type: :wiki, errors: e.message } end diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb index 5b32fca00a4..9c9e6668e6f 100644 --- a/lib/gitlab/changes_list.rb +++ b/lib/gitlab/changes_list.rb @@ -16,6 +16,7 @@ module Gitlab @changes ||= begin @raw_changes.map do |change| next if change.blank? + oldrev, newrev, ref = change.strip.split(' ') { oldrev: oldrev, newrev: newrev, ref: ref } end.compact diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index a788fb3fcbc..0bbd60d8ffe 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -98,6 +98,7 @@ module Gitlab def read_string(gz) string_size = read_uint32(gz) return nil unless string_size + gz.read(string_size) end diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb index 22941d48edf..5b2f09e03ea 100644 --- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -43,6 +43,7 @@ module Gitlab def parent return nil unless has_parent? + self.class.new(@path.to_s.chomp(basename), @entries) end @@ -64,6 +65,7 @@ module Gitlab def directories(opts = {}) return [] unless directory? + dirs = children.select(&:directory?) return dirs unless has_parent? && opts[:parent] @@ -74,6 +76,7 @@ module Gitlab def files return [] unless directory? + children.select(&:file?) end diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index b88b2e36d53..c811f88f483 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -8,6 +8,7 @@ module Gitlab def from_image(job) image = Gitlab::Ci::Build::Image.new(job.options[:image]) return unless image.valid? + image end diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 6555c589173..2844be80a84 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -37,6 +37,7 @@ module Gitlab def value return { name: @config } if string? return @config if hash? + {} end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 0159179f0a9..eb606b57667 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -111,6 +111,7 @@ module Gitlab def validate_string_or_regexp(value) return false unless value.is_a?(String) return validate_regexp(value) if look_like_regexp?(value) + true end end diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb index f07fd1dfdda..633de9f9776 100644 --- a/lib/gitlab/daemon.rb +++ b/lib/gitlab/daemon.rb @@ -2,6 +2,7 @@ module Gitlab class Daemon def self.initialize_instance(*args) raise "#{name} singleton instance already initialized" if @instance + @instance = new(*args) Kernel.at_exit(&@instance.method(:stop)) @instance diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 2c35da8f1aa..c276c3566b4 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -220,6 +220,15 @@ module Gitlab # column - The name of the column to update. # value - The value for the column. # + # The `value` argument is typically a literal. To perform a computed + # update, an Arel literal can be used instead: + # + # update_value = Arel.sql('bar * baz') + # + # update_column_in_batches(:projects, :foo, update_value) do |table, query| + # query.where(table[:some_column].eq('hello')) + # end + # # Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop # determines this method to be too complex while there's no way to make it # less "complex" without introducing extra methods (which actually will diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb index 5481024db8e..7e492938eac 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb @@ -68,6 +68,11 @@ module Gitlab has_one :route, as: :source self.table_name = 'projects' + HASHED_STORAGE_FEATURES = { + repository: 1, + attachments: 2 + }.freeze + def repository_storage_path Gitlab.config.repositories.storages[repository_storage]['path'] end @@ -76,6 +81,13 @@ module Gitlab def self.name 'Project' end + + def hashed_storage?(feature) + raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature) + return false unless respond_to?(:storage_version) + + self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature] + end end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb index 75a75f61953..d32616862f0 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -22,9 +22,11 @@ module Gitlab end def move_project_folders(project, old_full_path, new_full_path) - move_repository(project, old_full_path, new_full_path) - move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") - move_uploads(old_full_path, new_full_path) + unless project.hashed_storage?(:repository) + move_repository(project, old_full_path, new_full_path) + move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") + end + move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments) move_pages(old_full_path, new_full_path) end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index ea5891a028a..d0cfe2386ca 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -25,6 +25,10 @@ module Gitlab @repository = repository @diff_refs = diff_refs @fallback_diff_refs = fallback_diff_refs + + # Ensure items are collected in the the batch + new_blob + old_blob end def position(position_marker, position_type: :text) @@ -95,21 +99,15 @@ module Gitlab end def new_blob - return @new_blob if defined?(@new_blob) - - sha = new_content_sha - return @new_blob = nil unless sha + return unless new_content_sha - @new_blob = repository.blob_at(sha, file_path) + Blob.lazy(repository.project, new_content_sha, file_path) end def old_blob - return @old_blob if defined?(@old_blob) - - sha = old_content_sha - return @old_blob = nil unless sha + return unless old_content_sha - @old_blob = repository.blob_at(sha, old_path) + Blob.lazy(repository.project, old_content_sha, old_path) end def content_sha diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index 88ae65cb468..a6007ebf531 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -22,10 +22,7 @@ module Gitlab end def diff_files - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445 - Gitlab::GitalyClient.allow_n_plus_1_calls do - @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } - end + @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } end def diff_file_with_old_path(old_path) diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 55708d42161..2d7b57120a6 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -102,6 +102,7 @@ module Gitlab new_char = b[pos] break if old_char != new_char + length += 1 end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 7dc9cc7c281..8302f30a0a2 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -30,6 +30,7 @@ module Gitlab line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 next if line_old <= 1 && line_new <= 1 # top of file + yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index ccfb908bcca..690b27cde81 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -125,6 +125,7 @@ module Gitlab def find_diff_file(repository) return unless diff_refs.complete? return unless comparison = diff_refs.compare_in(repository.project) + comparison.diffs(paths: paths, expanded: true).diff_files.first end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index efc2e46d289..3a6165f504c 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -31,16 +31,22 @@ module Gitlab def check ensure_patches_dir - generate_patch(ce_branch, ce_patch_full_path) + add_remote('canonical-ce', "#{DEFAULT_CE_PROJECT_URL}.git") + generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, remote: 'canonical-ce') ensure_ee_repo Dir.chdir(ee_repo_dir) do step("In the #{ee_repo_dir} directory") + add_remote('canonical-ee', EE_REPO_URL) + status = catch(:halt_check) do ce_branch_compat_check! delete_ee_branches_locally! ee_branch_presence_check! + + step("Checking out #{ee_branch_found}", %W[git checkout -b #{ee_branch_found} canonical-ee/#{ee_branch_found}]) + generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, remote: 'canonical-ee') ee_branch_compat_check! end @@ -56,6 +62,13 @@ module Gitlab private + def add_remote(name, url) + step( + "Adding the #{name} remote (#{url})", + %W[git remote add #{name} #{url}] + ) + end + def ensure_ee_repo if Dir.exist?(ee_repo_dir) step("#{ee_repo_dir} already exists") @@ -71,14 +84,14 @@ module Gitlab FileUtils.mkdir_p(patches_dir) end - def generate_patch(branch, patch_path) + def generate_patch(branch:, patch_path:, remote:) FileUtils.rm(patch_path, force: true) - find_merge_base_with_master(branch: branch) + find_merge_base_with_master(branch: branch, master_remote: remote) step( - "Generating the patch against origin/master in #{patch_path}", - %w[git diff --binary origin/master...HEAD] + "Generating the patch against #{remote}/master in #{patch_path}", + %W[git diff --binary #{remote}/master...#{branch}] ) do |output, status| throw(:halt_check, :ko) unless status.zero? @@ -89,21 +102,21 @@ module Gitlab end def ce_branch_compat_check! - if check_patch(ce_patch_full_path).zero? + if check_patch(ce_patch_full_path, remote: 'canonical-ce').zero? puts applies_cleanly_msg(ce_branch) throw(:halt_check) end end def ee_branch_presence_check! - _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch origin #{ee_branch_prefix}]) + _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch canonical-ee #{ee_branch_prefix}]) if status.zero? @ee_branch_found = ee_branch_prefix return end - _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}]) + _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch canonical-ee #{ee_branch_suffix}]) if status.zero? @ee_branch_found = ee_branch_suffix @@ -116,11 +129,7 @@ module Gitlab end def ee_branch_compat_check! - step("Checking out origin/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} FETCH_HEAD]) - - generate_patch(ee_branch_found, ee_patch_full_path) - - unless check_patch(ee_patch_full_path).zero? + unless check_patch(ee_patch_full_path, remote: 'canonical-ee').zero? puts puts ee_branch_doesnt_apply_cleanly_msg @@ -131,10 +140,9 @@ module Gitlab puts applies_cleanly_msg(ee_branch_found) end - def check_patch(patch_path) + def check_patch(patch_path, remote:) step("Checking out master", %w[git checkout master]) - step("Resetting to latest master", %w[git reset --hard origin/master]) - step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo_url} #{ce_branch}]) + step("Resetting to latest master", %W[git reset --hard #{remote}/master]) step( "Checking if #{patch_path} applies cleanly to EE/master", # Don't use --check here because it can result in a 0-exit status even @@ -171,10 +179,10 @@ module Gitlab command(%W[git branch --delete --force #{ee_branch_suffix}]) end - def merge_base_found? + def merge_base_found?(master_remote:, branch:) step( - "Finding merge base with master", - %w[git merge-base origin/master HEAD] + "Finding merge base with #{master_remote}/master", + %W[git merge-base #{master_remote}/master #{branch}] ) do |output, status| if status.zero? puts "Merge base was found: #{output}" @@ -183,7 +191,7 @@ module Gitlab end end - def find_merge_base_with_master(branch:) + def find_merge_base_with_master(branch:, master_remote:) # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403) # In total we go (20 + 54 + 148 + 403 = 625) commits deeper depth = 20 @@ -192,19 +200,19 @@ module Gitlab depth += Math.exp(factor).to_i # Repository is initially cloned with a depth of 20 so we need to fetch # deeper in the case the branch has more than 20 commits on top of master - fetch(branch: branch, depth: depth) - fetch(branch: 'master', depth: depth, remote: DEFAULT_CE_PROJECT_URL) + fetch(branch: branch, depth: depth, remote: 'origin') + fetch(branch: 'master', depth: depth, remote: master_remote) - merge_base_found? + merge_base_found?(master_remote: master_remote, branch: branch) end - raise "\n#{branch} is too far behind master, please rebase it!\n" unless success + raise "\n#{branch} is too far behind #{master_remote}/master, please rebase it!\n" unless success end def fetch(branch:, depth:, remote: 'origin') step( "Fetching deeper...", - %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/origin/#{branch}] + %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/#{remote}/#{branch}] ) do |output, status| raise "Fetch failed: #{output}" unless status.zero? end @@ -304,8 +312,8 @@ module Gitlab 1. Create a new branch from master and cherry-pick your CE commits # In the EE repo - $ git fetch origin - $ git checkout -b #{ee_branch_prefix} origin/master + $ git fetch #{EE_REPO_URL} master + $ git checkout -b #{ee_branch_prefix} FETCH_HEAD $ git fetch #{ce_repo_url} #{ce_branch} $ git cherry-pick SHA # Repeat for all the commits you want to pick @@ -314,10 +322,9 @@ module Gitlab 2. Apply your branch's patch to EE # In the EE repo - $ git fetch origin master - $ git checkout -b #{ee_branch_prefix} origin/master - $ wget #{patch_url} - $ git apply --3way #{ce_patch_name} + $ git fetch #{EE_REPO_URL} master + $ git checkout -b #{ee_branch_prefix} FETCH_HEAD + $ wget #{patch_url} && git apply --3way #{ce_patch_name} At this point you might have conflicts such as: diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb index 5894384da5d..ea80e21532e 100644 --- a/lib/gitlab/email/handler/unsubscribe_handler.rb +++ b/lib/gitlab/email/handler/unsubscribe_handler.rb @@ -16,6 +16,7 @@ module Gitlab noteable = sent_notification.noteable raise NoteableNotFoundError unless noteable + noteable.unsubscribe(sent_notification.recipient) end diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb index 2152182b37f..acb000e3e23 100644 --- a/lib/gitlab/fogbugz_import/client.rb +++ b/lib/gitlab/fogbugz_import/client.rb @@ -45,6 +45,7 @@ module Gitlab project_name = repo(project_id).name res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events') return [] unless res['cases']['count'].to_i > 0 + res['cases']['case'] end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 3dcee681c72..5e426b13ade 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -18,6 +18,7 @@ module Gitlab def execute return true unless repo.valid? + client = Gitlab::FogbugzImport::Client.new(token: fb_session[:token], uri: fb_session[:uri]) @cases = client.cases(@repo.id.to_i) @@ -206,6 +207,7 @@ module Gitlab def format_content(raw_content) return raw_content if raw_content.nil? + linkify_issues(escape_for_markdown(raw_content)) end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index cc6c7609ec7..ddd52136bc4 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -102,6 +102,7 @@ module Gitlab if path_arr.size > 1 return nil unless entry[:type] == :tree + path_arr.shift find_entry_by_path(repository, entry[:oid], path_arr.join('/')) else @@ -178,6 +179,8 @@ module Gitlab ) end end + rescue Rugged::ReferenceError + nil end def rugged_raw(repository, sha, limit:) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index cfb88a0c12b..dcca20c75ef 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -304,7 +304,13 @@ module Gitlab end def delete_all_refs_except(prefixes) - delete_refs(*all_ref_names_except(prefixes)) + gitaly_migrate(:ref_delete_refs) do |is_enabled| + if is_enabled + gitaly_ref_client.delete_refs(except_with_prefixes: prefixes) + else + delete_refs(*all_ref_names_except(prefixes)) + end + end end # Returns an Array of all ref names, except when it's matching pattern @@ -984,6 +990,10 @@ module Gitlab @attributes.attributes(path) end + def gitattribute(path, name) + attributes(path)[name] + end + def languages(ref = nil) Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled| if is_enabled @@ -1048,12 +1058,11 @@ module Gitlab end def fetch_source_branch!(source_repository, source_branch, local_ref) - with_repo_branch_commit(source_repository, source_branch) do |commit| - if commit - write_ref(local_ref, commit.sha) - true + Gitlab::GitalyClient.migrate(:fetch_source_branch) do |is_enabled| + if is_enabled + gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref) else - false + rugged_fetch_source_branch(source_repository, source_branch, local_ref) end end end @@ -1151,6 +1160,11 @@ module Gitlab Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha) end + # Items should be of format [[commit_id, path], [commit_id1, path1]] + def batch_blobs(items, blob_size_limit: nil) + Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit) + end + def commit_index(user, branch_name, index, options) committer = user_to_committer(user) @@ -1201,6 +1215,17 @@ module Gitlab private + def rugged_fetch_source_branch(source_repository, source_branch, local_ref) + with_repo_branch_commit(source_repository, source_branch) do |commit| + if commit + write_ref(local_ref, commit.sha) + true + else + false + end + end + end + # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. def branches_filter(filter: nil, sort_by: nil) # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464 @@ -1376,6 +1401,7 @@ module Gitlab end return nil unless tmp_entry.type == :tree + tmp_entry = tmp_entry[dir] end end @@ -1496,6 +1522,7 @@ module Gitlab # Ref names must start with `refs/`. def rugged_ref_exists?(ref_name) raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/') + rugged.references.exist?(ref_name) rescue Rugged::ReferenceError false @@ -1562,6 +1589,7 @@ module Gitlab Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) rescue Rugged::ReferenceError => e raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/ + raise InvalidRef.new("Invalid reference #{start_point}") end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 637e7a0659c..4500482d68f 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -78,7 +78,7 @@ module Gitlab def list_remote_tags(remote) tag_list, exit_code, error = nil - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{full_path} ls-remote --tags #{remote}) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote}) Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr| tag_list = stdout.read @@ -88,7 +88,7 @@ module Gitlab raise RemoteError, error unless exit_code.zero? - tag_list.split('\n') + tag_list.split("\n") end end end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 022d1f249a9..d4a53d32c28 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -58,12 +58,12 @@ module Gitlab end end - def pages - @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled| + def pages(limit: nil) + @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| if is_enabled gitaly_get_all_pages else - gollum_get_all_pages + gollum_get_all_pages(limit: limit) end end end @@ -88,14 +88,23 @@ module Gitlab end end - def page_versions(page_path) + # options: + # :page - The Integer page number. + # :per_page - The number of items per page. + # :limit - Total number of items to return. + def page_versions(page_path, options = {}) current_page = gollum_page_by_path(page_path) - current_page.versions.map do |gollum_git_commit| - gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id) - new_version(gollum_page, gollum_git_commit.id) + + commits_from_page(current_page, options).map do |gitlab_git_commit| + gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id) + Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format) end end + def count_page_versions(page_path) + @repository.count_commits(ref: 'HEAD', path: page_path) + end + def preview_slug(title, format) # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid # using Rugged through a Gollum::Wiki instance @@ -110,6 +119,22 @@ module Gitlab private + # options: + # :page - The Integer page number. + # :per_page - The number of items per page. + # :limit - Total number of items to return. + def commits_from_page(gollum_page, options = {}) + unless options[:limit] + options[:offset] = ([1, options.delete(:page).to_i].max - 1) * Gollum::Page.per_page + options[:limit] = (options.delete(:per_page) || Gollum::Page.per_page).to_i + end + + @repository.log(ref: gollum_page.last_version.id, + path: gollum_page.path, + limit: options[:limit], + offset: options[:offset]) + end + def gollum_wiki @gollum_wiki ||= Gollum::Wiki.new(@repository.path) end @@ -126,8 +151,17 @@ module Gitlab end def new_version(gollum_page, commit_id) - commit = Gitlab::Git::Commit.find(@repository, commit_id) - Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format) + Gitlab::Git::WikiPageVersion.new(version(commit_id), gollum_page&.format) + end + + def version(commit_id) + commit_find_proc = -> { Gitlab::Git::Commit.find(@repository, commit_id) } + + if RequestStore.active? + RequestStore.fetch([:wiki_version_commit, commit_id]) { commit_find_proc.call } + else + commit_find_proc.call + end end def assert_type!(object, klass) @@ -185,8 +219,8 @@ module Gitlab Gitlab::Git::WikiFile.new(gollum_file) end - def gollum_get_all_pages - gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) } + def gollum_get_all_pages(limit: nil) + gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) } end def gitaly_write_page(name, format, content, commit_details) diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 0b35a787e07..572f4c892f6 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -75,6 +75,10 @@ module Gitlab address end + def self.address_metadata(storage) + Base64.strict_encode64(JSON.dump({ storage => { 'address' => address(storage), 'token' => token(storage) } })) + end + # All Gitaly RPC call sites should use GitalyClient.call. This method # makes sure that per-request authentication headers are set. # @@ -89,18 +93,19 @@ module Gitlab # kwargs.merge(deadline: Time.now + 10) # end # - def self.call(storage, service, rpc, request) + def self.call(storage, service, rpc, request, remote_storage: nil) start = Process.clock_gettime(Process::CLOCK_MONOTONIC) enforce_gitaly_request_limits(:call) - kwargs = request_kwargs(storage) + kwargs = request_kwargs(storage, remote_storage: remote_storage) kwargs = yield(kwargs) if block_given? + stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend ensure self.query_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start end - def self.request_kwargs(storage) + def self.request_kwargs(storage, remote_storage: nil) encoded_token = Base64.strict_encode64(token(storage).to_s) metadata = { 'authorization' => "Bearer #{encoded_token}", @@ -110,6 +115,7 @@ module Gitlab feature_stack = Thread.current[:gitaly_feature_stack] feature = feature_stack && feature_stack[0] metadata['call_site'] = feature.to_s if feature + metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage { metadata: metadata } end diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb new file mode 100644 index 00000000000..198a1de91c7 --- /dev/null +++ b/lib/gitlab/gitaly_client/attributes_bag.rb @@ -0,0 +1,31 @@ +module Gitlab + module GitalyClient + # This module expects an `ATTRS` const to be defined on the subclass + # See GitalyClient::WikiFile for an example + module AttributesBag + extend ActiveSupport::Concern + + included do + attr_accessor(*const_get(:ATTRS)) + end + + def initialize(params) + params = params.with_indifferent_access + + attributes.each do |attr| + instance_variable_set("@#{attr}", params[attr]) + end + end + + def ==(other) + attributes.all? do |field| + instance_variable_get("@#{field}") == other.instance_variable_get("@#{field}") + end + end + + def attributes + self.class.const_get(:ATTRS) + end + end + end +end diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb index 54df6304865..d98a0ce988f 100644 --- a/lib/gitlab/gitaly_client/diff.rb +++ b/lib/gitlab/gitaly_client/diff.rb @@ -1,21 +1,9 @@ module Gitlab module GitalyClient class Diff - FIELDS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze + ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze - attr_accessor(*FIELDS) - - def initialize(params) - params.each do |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) # rubocop:disable GitlabSecurity/PublicSend - end - end + include AttributesBag end end end diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb index 65d81dc5d46..da243ee2d1a 100644 --- a/lib/gitlab/gitaly_client/diff_stitcher.rb +++ b/lib/gitlab/gitaly_client/diff_stitcher.rb @@ -12,7 +12,7 @@ module Gitlab @rpc_response.each do |diff_msg| if current_diff.nil? - diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS) + diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::ATTRS) # gRPC uses frozen strings by default, and we need to have an unfrozen string as it # gets processed further down the line. So we unfreeze the first chunk of the patch # in case it's the only chunk we receive for this diff. diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index b0c73395cb1..31b04bc2650 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -126,6 +126,15 @@ module Gitlab GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request) end + def delete_refs(except_with_prefixes:) + request = Gitaly::DeleteRefsRequest.new( + repository: @gitaly_repo, + except_with_prefix: except_with_prefixes + ) + + GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request) + end + private def consume_refs_response(response) @@ -137,6 +146,7 @@ module Gitlab enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value + enum_value end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index cef692d3c2a..70cb16bd810 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -65,6 +65,25 @@ module Gitlab response.value end + + def fetch_source_branch(source_repository, source_branch, local_ref) + request = Gitaly::FetchSourceBranchRequest.new( + repository: @gitaly_repo, + source_repository: source_repository.gitaly_repository, + source_branch: source_branch.b, + target_ref: local_ref.b + ) + + response = GitalyClient.call( + @storage, + :repository_service, + :fetch_source_branch, + request, + remote_storage: source_repository.storage + ) + + response.result + end end end end diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb index a2e415864e6..47c60c92484 100644 --- a/lib/gitlab/gitaly_client/wiki_file.rb +++ b/lib/gitlab/gitaly_client/wiki_file.rb @@ -1,17 +1,9 @@ module Gitlab module GitalyClient class WikiFile - FIELDS = %i(name mime_type path raw_data).freeze + ATTRS = %i(name mime_type path raw_data).freeze - attr_accessor(*FIELDS) - - def initialize(params) - params = params.with_indifferent_access - - FIELDS.each do |field| - instance_variable_set("@#{field}", params[field]) - end - end + include AttributesBag end end end diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb index 98d96fe6211..7339468e911 100644 --- a/lib/gitlab/gitaly_client/wiki_page.rb +++ b/lib/gitlab/gitaly_client/wiki_page.rb @@ -1,16 +1,12 @@ module Gitlab module GitalyClient class WikiPage - FIELDS = %i(title format url_path path name historical raw_data).freeze + ATTRS = %i(title format url_path path name historical raw_data).freeze - attr_accessor(*FIELDS) + include AttributesBag def initialize(params) - params = params.with_indifferent_access - - FIELDS.each do |field| - instance_variable_set("@#{field}", params[field]) - end + super # All gRPC strings in a response are frozen, so we get an unfrozen # version here so appending to `raw_data` doesn't blow up. diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 8f05f40365e..c8f065f5881 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -94,6 +94,7 @@ module Gitlab page, version = wiki_page_from_iterator(response) { |message| message.end_of_page } break unless page && version + pages << [page, version] end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index f1007daab5d..075b3982608 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -65,6 +65,7 @@ module Gitlab y << item end break if items.empty? || items.size < per_page + page += 1 end end diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb index 61db4bd9ccc..f3d7407383c 100644 --- a/lib/gitlab/import_export/merge_request_parser.rb +++ b/lib/gitlab/import_export/merge_request_parser.rb @@ -1,7 +1,7 @@ module Gitlab module ImportExport class MergeRequestParser - FORKED_PROJECT_ID = -1 + FORKED_PROJECT_ID = nil def initialize(project, diff_head_sha, merge_request, relation_hash) @project = project diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb index c8479fbc0e8..fbbddb7bffa 100644 --- a/lib/gitlab/kubernetes/namespace.rb +++ b/lib/gitlab/kubernetes/namespace.rb @@ -12,6 +12,7 @@ module Gitlab @client.get_namespace(name) rescue ::KubeException => ke raise ke unless ke.error_code == 404 + false end diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index ed1de73f8c6..7274d1c3b43 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -62,6 +62,7 @@ module Gitlab def user return nil unless ldap_user + Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider) end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 4d5c67ed892..3945df27eed 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -9,11 +9,8 @@ module Gitlab class User < Gitlab::OAuth::User class << self def find_by_uid_and_provider(uid, provider) - uid = Gitlab::LDAP::Person.normalize_dn(uid) + identity = ::Identity.with_extern_uid(provider, uid).take - identity = ::Identity - .where(provider: provider) - .where(extern_uid: uid).last identity && identity.user end end diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb index 12c968805f5..4d096e5a741 100644 --- a/lib/gitlab/legacy_github_import/importer.rb +++ b/lib/gitlab/legacy_github_import/importer.rb @@ -15,6 +15,7 @@ module Gitlab def client return @client if defined?(@client) + unless credentials raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb index 8b5a60e6b8b..436a9e9550d 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -96,6 +96,7 @@ module Gitlab def worker_label return {} unless defined?(Unicorn::Worker) + worker_no = ::Prometheus::Client::Support::Unicorn.worker_id if worker_no diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 064299f40c8..ead1acb8d44 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -7,6 +7,7 @@ module Gitlab def sql(event) return unless current_transaction + metric_sql_duration_seconds.observe(current_transaction.labels, event.duration / 1000.0) current_transaction.increment(:sql_duration, event.duration, false) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index cfc6b2a2029..c6a56277922 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -42,12 +42,11 @@ module Gitlab project_url = URI.join(config.gitlab.url, path) import_prefix = strip_url(project_url.to_s) - repository_url = case current_application_settings.enabled_git_access_protocol - when 'ssh' + repository_url = if current_application_settings.enabled_git_access_protocol == 'ssh' shell = config.gitlab_shell port = ":#{shell.ssh_port}" unless shell.ssh_port == 22 "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git" - when 'http', nil + else "#{project_url}.git" end @@ -66,6 +65,7 @@ module Gitlab project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX) return unless project_path_match + path = project_path_match[1] # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`. diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb index 5e4932e4e57..c26656704d7 100644 --- a/lib/gitlab/middleware/read_only.rb +++ b/lib/gitlab/middleware/read_only.rb @@ -58,7 +58,7 @@ module Gitlab end def last_visited_url - @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url + @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url end def route_hash @@ -74,10 +74,16 @@ module Gitlab end def grack_route + # Calling route_hash may be expensive. Only do it if we think there's a possible match + return false unless request.path.end_with?('.git/git-upload-pack') + route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack' end def lfs_route + # Calling route_hash may be expensive. Only do it if we think there's a possible match + return false unless request.path.end_with?('/info/lfs/objects/batch') + route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch' end end diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb index eb3c9002710..c22d0a84860 100644 --- a/lib/gitlab/multi_collection_paginator.rb +++ b/lib/gitlab/multi_collection_paginator.rb @@ -55,7 +55,9 @@ module Gitlab def first_collection_last_page_size return @first_collection_last_page_size if defined?(@first_collection_last_page_size) - @first_collection_last_page_size = paginated_first_collection(first_collection_page_count).count + @first_collection_last_page_size = paginated_first_collection(first_collection_page_count) + .except(:select) + .size end end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index b4b3b00c84d..552133234a3 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -157,7 +157,7 @@ module Gitlab end def find_by_uid_and_provider - identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid) + identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take identity && identity.user end diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb index 962ff4d3985..1d9a5d1a20a 100644 --- a/lib/gitlab/optimistic_locking.rb +++ b/lib/gitlab/optimistic_locking.rb @@ -11,6 +11,7 @@ module Gitlab rescue ActiveRecord::StaleObjectError retries -= 1 raise unless retries >= 0 + subject.reload end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index bd677ec4bf3..2c7b8af83f2 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -25,7 +25,7 @@ module Gitlab # See https://github.com/docker/distribution/blob/master/reference/regexp.go. # def container_repository_name_regex - @container_repository_regex ||= %r{\A[a-z0-9]+(?:[-._/][a-z0-9]+)*\Z} + @container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-])[a-z0-9]+)*\Z} end ## diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb index 910533076b0..2c994536060 100644 --- a/lib/gitlab/routing.rb +++ b/lib/gitlab/routing.rb @@ -46,10 +46,10 @@ module Gitlab # Only replace the last occurence of `path`. # # `request.fullpath` includes the querystring - path = request.path.sub(%r{/#{path}/*(?!.*#{path})}, "/-/#{path}/") - path << "?#{request.query_string}" if request.query_string.present? + new_path = request.path.sub(%r{/#{path}(/*)(?!.*#{path})}, "/-/#{path}\\1") + new_path << "?#{request.query_string}" if request.query_string.present? - path + new_path end paths.each do |path| diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index e0a9d1dee77..d8faf7aad8c 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -28,6 +28,7 @@ module Gitlab def changed? return true unless gl_user + gl_user.changed? || gl_user.identities.any?(&:changed?) end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index a37112ae5c4..996d213fdb4 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -101,8 +101,7 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def import_repository(storage, name, url) - # Timeout should be less than 900 ideally, to prevent the memory killer - # to silently kill the process without knowing we are timing out here. + # The timeout ensures the subprocess won't hang forever cmd = [gitlab_shell_projects_path, 'import-project', storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"] gitlab_shell_fast_execute_raise_error(cmd) @@ -368,6 +367,7 @@ module Gitlab output, status = gitlab_shell_fast_execute_helper(cmd, vars) raise Error, output unless status.zero? + true end diff --git a/lib/gitlab/shell_adapter.rb b/lib/gitlab/shell_adapter.rb index fbe2a7a0d72..053dd4ab9e0 100644 --- a/lib/gitlab/shell_adapter.rb +++ b/lib/gitlab/shell_adapter.rb @@ -5,7 +5,7 @@ module Gitlab module ShellAdapter def gitlab_shell - Gitlab::Shell.new + @gitlab_shell ||= Gitlab::Shell.new end end end diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb index 11aeec1ebfa..f9faa134206 100644 --- a/lib/gitlab/string_range_marker.rb +++ b/lib/gitlab/string_range_marker.rb @@ -90,6 +90,7 @@ module Gitlab # Takes an array of integers, and returns an array of ranges covering the same integers def collapse_ranges(positions) return [] if positions.empty? + ranges = [] start = prev = positions[0] diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb index cb7957e2af9..33f07fa0120 100644 --- a/lib/gitlab/template/finders/repo_template_finder.rb +++ b/lib/gitlab/template/finders/repo_template_finder.rb @@ -18,6 +18,7 @@ module Gitlab def read(path) blob = @repository.blob_at(@commit.id, path) if @commit raise FileNotFoundError if blob.nil? + blob.data end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 1caa791c1be..59331c827af 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -70,6 +70,7 @@ module Gitlab def generate_full_url return @url unless valid_credentials? + @full_url = @url.dup @full_url.password = credentials[:password] if credentials[:password].present? diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index c60bd91ea6e..11472ce6cce 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -99,6 +99,7 @@ module Gitlab def level_value(level) return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i) + string_options[level] || PRIVATE end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index e1219df1b25..864a9e04888 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -174,6 +174,7 @@ module Gitlab @secret ||= begin bytes = Base64.strict_decode64(File.read(secret_path).chomp) raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH + bytes end end diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb index 05668c69006..f5485eb89fa 100644 --- a/lib/haml_lint/inline_javascript.rb +++ b/lib/haml_lint/inline_javascript.rb @@ -9,6 +9,7 @@ unless Rails.env.production? def visit_filter(node) return unless node.filter_type == 'javascript' + record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)') end end diff --git a/lib/milestone_array.rb b/lib/milestone_array.rb new file mode 100644 index 00000000000..4ed8485b36a --- /dev/null +++ b/lib/milestone_array.rb @@ -0,0 +1,40 @@ +module MilestoneArray + class << self + def sort(array, sort_method) + case sort_method + when 'due_date_asc' + sort_asc_nulls_last(array, 'due_date') + when 'due_date_desc' + sort_desc_nulls_last(array, 'due_date') + when 'start_date_asc' + sort_asc_nulls_last(array, 'start_date') + when 'start_date_desc' + sort_desc_nulls_last(array, 'start_date') + when 'name_asc' + sort_asc(array, 'title') + when 'name_desc' + sort_asc(array, 'title').reverse + else + array + end + end + + private + + def sort_asc_nulls_last(array, attribute) + attribute = attribute.to_sym + + array.select(&attribute).sort_by(&attribute) + array.reject(&attribute) + end + + def sort_desc_nulls_last(array, attribute) + attribute = attribute.to_sym + + array.select(&attribute).sort_by(&attribute).reverse + array.reject(&attribute) + end + + def sort_asc(array, attribute) + array.sort_by(&attribute.to_sym) + end + end +end diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb deleted file mode 100644 index 939b23a3421..00000000000 --- a/lib/rouge/lexers/math.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Rouge - module Lexers - class Math < PlainText - title "A passthrough lexer used for LaTeX input" - desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter" - tag 'math' - end - end -end diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb deleted file mode 100644 index 63c461764fc..00000000000 --- a/lib/rouge/lexers/plantuml.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Rouge - module Lexers - class Plantuml < PlainText - title "A passthrough lexer used for PlantUML input" - desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter" - tag 'plantuml' - end - end -end diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index 00221f77cf4..8b145fb4511 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -24,6 +24,7 @@ module SystemCheck # @param [BaseCheck] check class def <<(check) raise ArgumentError unless check.is_a?(Class) && check < BaseCheck + @checks << check end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 8ae1b6a626a..301affc9522 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -59,7 +59,11 @@ namespace :gitlab do .sub(%r{^/*}, '') .chomp('.git') .chomp('.wiki') - next if Project.find_by_full_path(repo_with_namespace) + + # TODO ignoring hashed repositories for now. But revisit to fully support + # possible orphaned hashed repos + next if repo_with_namespace.start_with?('@hashed/') || Project.find_by_full_path(repo_with_namespace) + new_path = path + move_suffix puts path.inspect + ' -> ' + new_path.inspect File.rename(path, new_path) @@ -75,6 +79,7 @@ namespace :gitlab do User.find_each do |user| next unless user.ldap_user? + print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." if Gitlab::LDAP::Access.allowed?(user) puts " [OK]".color(:green) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 8377fe3269d..4d880c05f99 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -14,18 +14,18 @@ namespace :gitlab do checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir) + command = %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + _, 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 + command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? 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 -u BUNDLE_GEMFILE] + command) } + Bundler.with_original_env { run_command!(command) } end end end @@ -78,13 +78,18 @@ namespace :gitlab do config[:auth] = { token: 'secret' } if Rails.env.test? config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } + config[:bin_dir] = Gitlab.config.gitaly.client_path + TOML.dump(config) end def create_gitaly_configuration - File.open("config.toml", "w") do |f| + File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f| f.puts gitaly_configuration_toml end + rescue Errno::EEXIST + puts "Skipping config.toml generation:" + puts "A configuration file already exists." rescue ArgumentError => e puts "Skipping config.toml generation:" puts e.message diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake deleted file mode 100644 index 6cbc83b8973..00000000000 --- a/lib/tasks/gitlab/sidekiq.rake +++ /dev/null @@ -1,47 +0,0 @@ -namespace :gitlab do - namespace :sidekiq do - QUEUE = 'queue:post_receive'.freeze - - desc 'Drop all Sidekiq PostReceive jobs for a given project' - task :drop_post_receive, [:project] => :environment do |t, args| - unless args.project.present? - abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]" - end - project_path = Project.find_by_full_path(args.project).repository.path_to_repo - - Sidekiq.redis do |redis| - unless redis.exists(QUEUE) - abort "Queue #{QUEUE} is empty" - end - - temp_queue = "#{QUEUE}_#{Time.now.to_i}" - redis.rename(QUEUE, temp_queue) - - # At this point, then post_receive queue is empty. It may be receiving - # new jobs already. We will repopulate it with the old jobs, skipping the - # ones we want to drop. - dropped = 0 - while (job = redis.lpop(temp_queue)) - if repo_path(job) == project_path - dropped += 1 - else - redis.rpush(QUEUE, job) - end - end - # The temp_queue will delete itself after we have popped all elements - # from it - - puts "Dropped #{dropped} jobs containing #{project_path} from #{QUEUE}" - end - end - - def repo_path(job) - job_args = JSON.parse(job)['args'] - if job_args - job_args.first - else - nil - end - end - end -end diff --git a/package.json b/package.json index cbcce40ffb9..16a6e45e820 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "dependencies": { "autosize": "^4.0.0", "axios": "^0.16.2", + "axios-mock-adapter": "^1.10.0", "babel-core": "^6.22.1", "babel-eslint": "^7.2.1", "babel-loader": "^7.1.1", "babel-plugin-transform-define": "^1.2.0", "babel-preset-latest": "^6.24.0", "babel-preset-stage-2": "^6.22.0", + "blackst0ne-mermaid": "^7.1.0-fixed", "bootstrap-sass": "^3.3.6", "brace-expansion": "^1.1.8", "compression-webpack-plugin": "^1.0.0", diff --git a/qa/Dockerfile b/qa/Dockerfile index f3a81a7e355..9b6ffff7c4d 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -9,6 +9,13 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list RUN apt-get update && apt-get install -y wget git unzip xvfb ## +# Install Docker +# +RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \ + tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \ + rm docker-17.09.0-ce.tgz + +## # Install Google Chrome version with headless support # RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add - @@ -49,6 +49,10 @@ module QA module Sandbox autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare' end + + module Admin + autoload :HashedStorage, 'qa/scenario/gitlab/admin/hashed_storage' + end end end @@ -64,9 +68,11 @@ module QA autoload :Entry, 'qa/page/main/entry' autoload :Login, 'qa/page/main/login' autoload :Menu, 'qa/page/main/menu' + autoload :OAuth, 'qa/page/main/oauth' end module Dashboard + autoload :Projects, 'qa/page/dashboard/projects' autoload :Groups, 'qa/page/dashboard/groups' end @@ -82,6 +88,7 @@ module QA module Admin autoload :Menu, 'qa/page/admin/menu' + autoload :Settings, 'qa/page/admin/settings' end module Mattermost @@ -98,6 +105,13 @@ module QA end ## + # Classes describing shell interaction with GitLab + # + module Shell + autoload :Omnibus, 'qa/shell/omnibus' + end + + ## # Classes that make it possible to execute features tests. # module Specs diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index baa06b1c75e..dd289ffe269 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -3,8 +3,11 @@ module QA module Admin class Menu < Page::Base def go_to_license - link = find_link 'License' - link.click + click_link 'License' + end + + def go_to_settings + click_link 'Settings' end end end diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb new file mode 100644 index 00000000000..39e2f2062ad --- /dev/null +++ b/qa/qa/page/admin/settings.rb @@ -0,0 +1,18 @@ +module QA + module Page + module Admin + class Settings < Page::Base + def enable_hashed_storage + scroll_to 'legend', text: 'Repository Storage' + check 'Create new projects using hashed storage paths' + end + + def save_settings + scroll_to '.form-actions' do + click_button 'Save' + end + end + end + end + end +end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index bdddfb877c5..f9a93ef051e 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -1,3 +1,5 @@ +require 'capybara/dsl' + module QA module Page class Base @@ -7,6 +9,21 @@ module QA def refresh visit current_url end + + def scroll_to(selector, text: nil) + page.execute_script <<~JS + var elements = Array.from(document.querySelectorAll('#{selector}')); + var text = '#{text}'; + + if (text.length > 0) { + elements.find(e => e.textContent === text).scrollIntoView(); + } else { + elements[0].scrollIntoView(); + } + JS + + page.within(selector) { yield } if block_given? + end end end end diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb new file mode 100644 index 00000000000..7ed27da6d89 --- /dev/null +++ b/qa/qa/page/dashboard/projects.rb @@ -0,0 +1,11 @@ +module QA + module Page + module Dashboard + class Projects < Page::Base + def go_to_project(name) + find_link(text: name).click + end + end + end + end +end diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb index ac939732b1d..ae6484b4bfe 100644 --- a/qa/qa/page/main/entry.rb +++ b/qa/qa/page/main/entry.rb @@ -16,6 +16,7 @@ module QA while Time.now - start < 240 break if page.has_css?('.application', wait: 10) + refresh end end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 178c5ea6930..bc9c4ec1215 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -7,7 +7,10 @@ module QA end def go_to_projects - within_top_menu { click_link 'Projects' } + within_top_menu do + click_link 'Projects' + click_link 'Your projects' + end end def go_to_admin_area diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb new file mode 100644 index 00000000000..e746cff0a80 --- /dev/null +++ b/qa/qa/page/main/oauth.rb @@ -0,0 +1,15 @@ +module QA + module Page + module Main + class OAuth < Page::Base + def needs_authorization? + page.current_url.include?('/oauth') + end + + def authorize! + click_button 'Authorize' + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 68d9597c4d2..3b2bac84f3f 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -14,6 +14,10 @@ module QA find('#project_clone').value end + def project_name + find('.project-title').text + end + def wait_for_push sleep 5 end diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb index cf8996cd597..d6de4d404c8 100644 --- a/qa/qa/scenario/bootable.rb +++ b/qa/qa/scenario/bootable.rb @@ -28,7 +28,7 @@ module QA private - def attribute(name, arg, desc) + def attribute(name, arg, desc = '') options.push(Option.new(name, arg, desc)) end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb index ae099fd911e..b9d924651a0 100644 --- a/qa/qa/scenario/entrypoint.rb +++ b/qa/qa/scenario/entrypoint.rb @@ -8,6 +8,7 @@ module QA include Bootable def perform(address, *files) + Specs::Config.act { configure_capybara! } Runtime::Scenario.define(:gitlab_address, address) ## diff --git a/qa/qa/scenario/gitlab/admin/hashed_storage.rb b/qa/qa/scenario/gitlab/admin/hashed_storage.rb new file mode 100644 index 00000000000..ac2cd549829 --- /dev/null +++ b/qa/qa/scenario/gitlab/admin/hashed_storage.rb @@ -0,0 +1,25 @@ +module QA + module Scenario + module Gitlab + module Admin + class HashedStorage < Scenario::Template + def perform(*traits) + raise ArgumentError unless traits.include?(:enabled) + + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Menu.act { go_to_admin_area } + Page::Admin::Menu.act { go_to_settings } + + Page::Admin::Settings.act do + enable_hashed_storage + save_settings + end + + QA::Page::Main::Menu.act { sign_out } + end + end + end + end + end +end diff --git a/qa/qa/shell/omnibus.rb b/qa/qa/shell/omnibus.rb new file mode 100644 index 00000000000..6b3628d3109 --- /dev/null +++ b/qa/qa/shell/omnibus.rb @@ -0,0 +1,39 @@ +require 'open3' + +module QA + module Shell + class Omnibus + include Scenario::Actable + + def initialize(container) + @name = container + end + + def gitlab_ctl(command, input: nil) + if input.nil? + shell "docker exec #{@name} gitlab-ctl #{command}" + else + shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" + end + end + + private + + ## + # TODO, make it possible to use generic QA framework classes + # as a library - gitlab-org/gitlab-qa#94 + # + def shell(command) + puts "Executing `#{command}`" + + Open3.popen2e(command) do |_in, out, wait| + out.each { |line| puts line } + + if wait.value.exited? && wait.value.exitstatus.nonzero? + raise "Docker command `#{command}` failed!" + end + end + end + end + end +end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 9f9fe9844d2..bce7923e52d 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -9,6 +9,8 @@ require 'selenium-webdriver' module QA module Specs class Config < Scenario::Template + include Scenario::Actable + def perform configure_rspec! configure_capybara! diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index 92f91cb2725..1fde3f89a99 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -9,5 +9,16 @@ module QA expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) end end + + ## + # TODO, temporary workaround for gitlab-org/gitlab-qa#102. + # + after do + visit Runtime::Scenario.mattermost_address + reset_session! + + visit Runtime::Scenario.gitlab_address + reset_session! + end end end diff --git a/rubocop/cop/line_break_after_guard_clauses.rb b/rubocop/cop/line_break_after_guard_clauses.rb new file mode 100644 index 00000000000..67477f064ab --- /dev/null +++ b/rubocop/cop/line_break_after_guard_clauses.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Ensures a line break after guard clauses. + # + # @example + # # bad + # return unless condition + # do_stuff + # + # # good + # return unless condition + # + # do_stuff + # + # # bad + # raise if condition + # do_stuff + # + # # good + # raise if condition + # + # do_stuff + # + # Multiple guard clauses are allowed without + # line break. + # + # # good + # return unless condition_a + # return unless condition_b + # + # do_stuff + # + # Guard clauses in case statement are allowed without + # line break. + # + # # good + # case model + # when condition_a + # return true unless condition_b + # when + # ... + # end + # + # Guard clauses before end are allowed without + # line break. + # + # # good + # if condition_a + # do_something + # else + # do_something_else + # return unless condition + # end + # + # do_something_more + class LineBreakAfterGuardClauses < RuboCop::Cop::Cop + MSG = 'Add a line break after guard clauses' + + def_node_matcher :guard_clause_node?, <<-PATTERN + [{(send nil? {:raise :fail :throw} ...) return break next} single_line?] + PATTERN + + def on_if(node) + return unless node.single_line? + return unless guard_clause?(node) + return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node)) + + add_offense(node, :expression, MSG) + end + + def autocorrect(node) + lambda do |corrector| + corrector.insert_after(node.loc.expression, "\n") + end + end + + private + + def guard_clause?(node) + return false unless node.if_type? + + guard_clause_node?(node.if_branch) + end + + def next_sibling(node) + node.parent.children[node.sibling_index + 1] + end + + def next_line(node) + processed_source[node.loc.line] + end + + def clause_last_line?(line) + line =~ /^\s*(?:end|elsif|else|when|rescue|ensure)/ + end + end + end +end diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/update_large_table.rb index fb363f95b56..3ae3fb1b68e 100644 --- a/rubocop/cop/migration/add_column_with_default_to_large_table.rb +++ b/rubocop/cop/migration/update_large_table.rb @@ -12,12 +12,12 @@ module RuboCop # # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more # information. - class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop + class UpdateLargeTable < RuboCop::Cop::Cop include MigrationHelpers - MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \ - 'long time to complete, and should be avoided unless absolutely ' \ - 'necessary'.freeze + MSG = 'Using `%s` on the `%s` table will take a long time to ' \ + 'complete, and should be avoided unless absolutely ' \ + 'necessary'.freeze LARGE_TABLES = %i[ ci_pipelines @@ -34,20 +34,22 @@ module RuboCop users ].freeze - def_node_matcher :add_column_with_default?, <<~PATTERN - (send nil :add_column_with_default $(sym ...) ...) + def_node_matcher :batch_update?, <<~PATTERN + (send nil ${:add_column_with_default :update_column_in_batches} $(sym ...) ...) PATTERN def on_send(node) return unless in_migration?(node) - matched = add_column_with_default?(node) - return unless matched + matches = batch_update?(node) + return unless matches + + update_method = matches.first + table = matches.last.to_a.first - table = matched.to_a.first return unless LARGE_TABLES.include?(table) - add_offense(node, :expression, format(MSG, table)) + add_offense(node, :expression, format(MSG, update_method, table)) end end end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 4ebbe010e90..7621ea50da9 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -3,11 +3,11 @@ require_relative 'cop/active_record_serialize' require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/in_batches' +require_relative 'cop/line_break_after_guard_clauses' require_relative 'cop/polymorphic_associations' require_relative 'cop/project_path_helper' require_relative 'cop/redirect_with_status' require_relative 'cop/migration/add_column' -require_relative 'cop/migration/add_column_with_default_to_large_table' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' @@ -20,6 +20,7 @@ require_relative 'cop/migration/reversible_add_column_with_default' require_relative 'cop/migration/safer_boolean_column' require_relative 'cop/migration/timestamps' require_relative 'cop/migration/update_column_in_batches' +require_relative 'cop/migration/update_large_table' require_relative 'cop/rspec/env_assignment' require_relative 'cop/rspec/single_line_hook' require_relative 'cop/rspec/verbose_include_metadata' diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs index d3a9f5ff4ea..a270823b857 100755 --- a/scripts/trigger-build-docs +++ b/scripts/trigger-build-docs @@ -27,14 +27,7 @@ def docs_branch # Prefix the remote branch with 'preview-' in order to avoid # name conflicts in the rare case the branch name already # exists in the docs repo and truncate to max length. - "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max] -end - -# -# Dummy way to find out in which repo we are, CE or EE -# -def ee? - File.exist?('CHANGELOG-EE.md') + "#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max] end # @@ -56,14 +49,34 @@ def remove_remote_branch end # +# Define suffix in review app URL based on project +# +def slug + case ENV["CI_PROJECT_NAME"] + when 'gitlab-ce' + 'ce' + when 'gitlab-ee' + 'ee' + when 'gitlab-runner' + 'runner' + when 'omnibus-gitlab' + 'omnibus' + end +end + +# +# Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml +# +def param_name + "BRANCH_#{slug.upcase}" +end + +# # Trigger a pipeline in gitlab-docs # def trigger_pipeline - # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml - param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE' - # The review app URL - app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}" + app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{slug}" # Create the pipeline puts "=> Triggering a pipeline..." diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b73ca0c2346..768c7e99c96 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -6,6 +6,10 @@ describe ApplicationController do describe '#check_password_expiration' do let(:controller) { described_class.new } + before do + allow(controller).to receive(:session).and_return({}) + end + it 'redirects if the user is over their password expiry' do user.password_expires_at = Time.new(2002) diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index 4262d474e59..cb1b460fc0e 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -280,6 +280,17 @@ describe Groups::ChildrenController do expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) end + + context 'with a mixed first page' do + let!(:first_page_subgroups) { [create(:group, :public, parent: group)] } + let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) } + + it 'correctly calculates the counts' do + get :index, group_id: group.to_param, sort: 'id_asc', page: 2, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end end end end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 5dc27e2bbba..fd90c0d8bad 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Projects::CommitController do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } let(:commit) { project.commit("master") } let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } let(:master_pickable_commit) { project.commit(master_pickable_sha) } before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'GET show' do diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 5f5a789d5cc..37e9f863fc4 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -336,6 +336,29 @@ describe Projects::NotesController do end end + describe 'PUT update' do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } + } + end + + before do + sign_in(note.author) + project.team << [note.author, :developer] + end + + it "updates the note" do + expect { put :update, request_params }.to change { note.reload.note } + end + end + describe 'DELETE destroy' do let(:request_params) do { diff --git a/spec/factories/fork_network_members.rb b/spec/factories/fork_network_members.rb new file mode 100644 index 00000000000..509c4e1fa1c --- /dev/null +++ b/spec/factories/fork_network_members.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :fork_network_member do + association :project + association :fork_network + + forked_from_project { fork_network.root_project } + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index f0d05504b7e..ab4ae123429 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -130,6 +130,7 @@ FactoryGirl.define do before(:create) do |note, evaluator| discussion = evaluator.in_reply_to next unless discussion + discussion = discussion.to_discussion if discussion.is_a?(Note) next unless discussion diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index b47f9055d29..a69b428d117 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -167,19 +167,36 @@ describe "Admin::Users" do it 'sees impersonation log out icon' do icon = first('.fa.fa-user-secret') - expect(icon).not_to eql nil + expect(icon).not_to be nil end it 'logs out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username) + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do find(:css, 'li.impersonation a').click - expect(current_path).to eql "/admin/users/#{another_user.username}" + expect(current_path).to eq("/admin/users/#{another_user.username}") + end + end + + context 'when impersonating a user with an expired password' do + before do + another_user.update(password_expires_at: Time.now - 5.minutes) + click_link 'Impersonate' + end + + it 'does not redirect to password change page' do + expect(current_path).to eq('/') + end + + it 'is redirected back to the impersonated users page in the admin after stopping' do + find(:css, 'li.impersonation a').click + + expect(current_path).to eq("/admin/users/#{another_user.username}") end end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 9137ab82ff4..205900615c4 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -331,11 +331,29 @@ describe 'Issue Boards', :js do context 'subscription' do it 'changes issue subscription' do click_card(card) + wait_for_requests - page.within('.subscription') do + page.within('.subscriptions') do click_button 'Subscribe' wait_for_requests - expect(page).to have_content("Unsubscribe") + + expect(page).to have_content('Unsubscribe') + end + end + + it 'has "Unsubscribe" button when already subscribed' do + create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true) + visit project_board_path(project, board) + wait_for_requests + + click_card(card) + wait_for_requests + + page.within('.subscriptions') do + click_button 'Unsubscribe' + wait_for_requests + + expect(page).to have_content('Subscribe') end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 479fb713297..98586ddbd81 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Commits' do - include CiStatusHelper - let(:project) { create(:project, :repository) } let(:user) { create(:user) } @@ -33,7 +31,7 @@ describe 'Commits' do describe 'Commit builds' do before do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it { expect(page).to have_content pipeline.sha[0..7] } @@ -79,7 +77,7 @@ describe 'Commits' do describe 'Commit builds', :js do before do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it 'shows pipeline`s data' do @@ -95,7 +93,7 @@ describe 'Commits' do end it do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) click_on 'Download artifacts' expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) end @@ -103,7 +101,7 @@ describe 'Commits' do describe 'Cancel all builds' do it 'cancels commit', :js do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) click_on 'Cancel running' expect(page).to have_content 'canceled' end @@ -111,7 +109,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build', :js do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) find('.js-btn-cancel-pipeline').click expect(page).to have_content 'canceled' end @@ -120,13 +118,13 @@ describe 'Commits' do describe '.gitlab-ci.yml not found warning' do context 'ci builds enabled' do it "does not show warning" do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' end it 'shows warning' do stub_ci_pipeline_yaml_file(nil) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) expect(page).to have_content '.gitlab-ci.yml not found in this commit' end end @@ -135,7 +133,7 @@ describe 'Commits' do before do stub_ci_builds_disabled stub_ci_pipeline_yaml_file(nil) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it 'does not show warning' do @@ -149,7 +147,7 @@ describe 'Commits' do before do project.team << [user, :reporter] build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it 'Renders header', :js do @@ -171,7 +169,7 @@ describe 'Commits' do visibility_level: Gitlab::VisibilityLevel::INTERNAL, public_builds: false) build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it do @@ -202,5 +200,12 @@ describe 'Commits' do expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}") end end + + it 'shows the ref switcher with the multi-file editor enabled', :js do + set_cookie('new_repo', 'true') + visit project_commits_path(project, branch_name) + + expect(find('.js-project-refs-dropdown')).to have_content branch_name + end end end diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb new file mode 100644 index 00000000000..a0fe40cf1d3 --- /dev/null +++ b/spec/features/groups/milestones_sorting_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +feature 'Milestones sorting', :js do + let(:group) { create(:group) } + let!(:project) { create(:project_empty_repo, group: group) } + let!(:other_project) { create(:project_empty_repo, group: group) } + let!(:project_milestone1) { create(:milestone, project: project, title: 'v1.0', due_date: 10.days.from_now) } + let!(:other_project_milestone1) { create(:milestone, project: other_project, title: 'v1.0', due_date: 10.days.from_now) } + let!(:project_milestone2) { create(:milestone, project: project, title: 'v2.0', due_date: 5.days.from_now) } + let!(:other_project_milestone2) { create(:milestone, project: other_project, title: 'v2.0', due_date: 5.days.from_now) } + let!(:group_milestone) { create(:milestone, group: group, title: 'v3.0', due_date: 7.days.from_now) } + let(:user) { create(:group_member, :master, user: create(:user), group: group ).user } + + before do + sign_in(user) + end + + scenario 'visit group milestones and sort by due_date_asc' do + visit group_milestones_path(group) + + expect(page).to have_button('Due soon') + + # assert default sorting + within '.milestones' do + expect(page.all('ul.content-list > li').first.text).to include('v2.0') + expect(page.all('ul.content-list > li')[1].text).to include('v3.0') + expect(page.all('ul.content-list > li').last.text).to include('v1.0') + end + + click_button 'Due soon' + + sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text) + + expect(sort_options[0]).to eq('Due soon') + expect(sort_options[1]).to eq('Due later') + expect(sort_options[2]).to eq('Start soon') + expect(sort_options[3]).to eq('Start later') + expect(sort_options[4]).to eq('Name, ascending') + expect(sort_options[5]).to eq('Name, descending') + + click_link 'Due later' + + expect(page).to have_button('Due later') + + within '.milestones' do + expect(page.all('ul.content-list > li').first.text).to include('v1.0') + expect(page.all('ul.content-list > li')[1].text).to include('v3.0') + expect(page.all('ul.content-list > li').last.text).to include('v2.0') + end + end +end diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb new file mode 100644 index 00000000000..e25fd1a6249 --- /dev/null +++ b/spec/features/issuables/shortcuts_issuable_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +feature 'Blob shortcuts', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:issue) { create(:issue, project: project, author: user) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:note_text) { 'I got this!' } + + before do + project.add_developer(user) + sign_in(user) + end + + describe 'pressing "r"' do + describe 'On an Issue' do + before do + create(:note, noteable: issue, project: project, note: note_text) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'quotes the selected text' do + select_element('.note-text') + find('body').native.send_key('r') + + expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text) + end + end + + describe 'On a Merge Request' do + before do + create(:note, noteable: merge_request, project: project, note: note_text) + visit project_merge_request_path(project, merge_request) + wait_for_requests + end + + it 'quotes the selected text' do + select_element('.note-text') + find('body').native.send_key('r') + + expect(find('.js-main-target-form #note_note').value).to include(note_text) + end + end + end +end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index b70d3060f05..cc1b187ff54 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -69,6 +69,12 @@ describe 'GitLab Markdown' do end end + it 'parses mermaid code block' do + aggregate_failures do + expect(doc).to have_selector('pre.code.js-render-mermaid') + end + end + it 'parses strikethroughs' do expect(doc).to have_selector(%{del:contains("and this text doesn't")}) end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index a55ecaa5697..b579e32c9aa 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -13,6 +13,8 @@ "confidential": { "type": "boolean" }, "due_date": { "type": ["date", "null"] }, "relative_position": { "type": "integer" }, + "issue_sidebar_endpoint": { "type": "string" }, + "toggle_subscription_endpoint": { "type": "string" }, "project": { "id": { "type": "integer" }, "path": { "type": "string" } diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 4f46e40ce7a..638cd8b07c8 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -268,3 +268,37 @@ However the wrapping tags can not be mixed as such - ### Videos ![My Video](/assets/videos/gitlab-demo.mp4) + +### Mermaid + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid + +It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid]. + +In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block. + +Example: + + ```mermaid + graph TD; + A-->B; + A-->C; + B-->D; + C-->D; + ``` + +Becomes: + +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` + +For details see the [Mermaid official page][mermaid]. + +[mermaid]: https://mermaidjs.github.io/ "Mermaid website" + diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 83b13b06dc1..8f607899b20 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -9,10 +9,11 @@ import Vue from 'vue'; import '~/boards/models/assignee'; +import eventHub from '~/boards/eventhub'; import '~/boards/models/list'; import '~/boards/models/label'; import '~/boards/stores/boards_store'; -import boardCard from '~/boards/components/board_card'; +import boardCard from '~/boards/components/board_card.vue'; import './mock_data'; describe('Board card', () => { @@ -157,33 +158,35 @@ describe('Board card', () => { }); it('sets detail issue to card issue on mouse up', () => { + spyOn(eventHub, '$emit'); + triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue); expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list); }); it('adds active class if detail issue is set', (done) => { - triggerEvent('mousedown'); - triggerEvent('mouseup'); - - setTimeout(() => { - expect(vm.$el.classList.contains('is-active')).toBe(true); - done(); - }, 0); + vm.detailIssue.issue = vm.issue; + + Vue.nextTick() + .then(() => { + expect(vm.$el.classList.contains('is-active')).toBe(true); + }) + .then(done) + .catch(done.fail); }); it('resets detail issue to empty if already set', () => { - triggerEvent('mousedown'); - triggerEvent('mouseup'); + spyOn(eventHub, '$emit'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + gl.issueBoards.BoardsStore.detail.issue = vm.issue; triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue'); }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 022d286d5df..ccde657789a 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -133,6 +133,19 @@ describe('Issue model', () => { expect(relativePositionIssue.position).toBe(1); }); + it('updates data', () => { + issue.updateData({ subscribed: true }); + expect(issue.subscribed).toBe(true); + }); + + it('sets fetching state', () => { + expect(issue.isFetching.subscriptions).toBe(true); + + issue.setFetchingState('subscriptions', false); + + expect(issue.isFetching.subscriptions).toBe(false); + }); + describe('update', () => { it('passes assignee ids when there are assignees', (done) => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 2ea290108a4..5662c7387fb 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -223,23 +223,46 @@ describe('Issuable output', () => { }); }); - it('closes form on error', (done) => { - spyOn(window, 'Flash').and.callThrough(); - spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { - reject(); - })); + describe('error when updating', () => { + beforeEach(() => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + }); - vm.updateIssuable(); + it('closes form on error', (done) => { + vm.updateIssuable(); - setTimeout(() => { - expect( - eventHub.$emit, - ).toHaveBeenCalledWith('close.form'); - expect( - window.Flash, - ).toHaveBeenCalledWith('Error updating issue'); + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating issue'); - done(); + done(); + }); + }); + + it('returns the correct error message for issuableType', (done) => { + vm.issuableType = 'merge request'; + + Vue.nextTick(() => { + vm.updateIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating merge request'); + + done(); + }); + }); }); }); }); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js index f6625b748b6..d779ab7bb31 100644 --- a/spec/javascripts/issue_show/components/edit_actions_spec.js +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -61,6 +61,15 @@ describe('Edit Actions components', () => { }); }); + it('should not show delete button if showDeleteButton is false', (done) => { + vm.showDeleteButton = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-danger')).toBeNull(); + done(); + }); + }); + describe('updateIssuable', () => { it('sends update.issauble event when clicking save button', () => { vm.$el.querySelector('.btn-save').click(); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js index 1d7fa7e12fc..3069a0cd60e 100644 --- a/spec/javascripts/jobs/job_details_mediator_spec.js +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -1,39 +1,35 @@ -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import JobMediator from '~/jobs/job_details_mediator'; import job from './mock_data'; describe('JobMediator', () => { let mediator; + let mock; beforeEach(() => { - mediator = new JobMediator({ endpoint: 'foo' }); + mediator = new JobMediator({ endpoint: 'jobs/40291672.json' }); + mock = new MockAdapter(axios); }); it('should set defaults', () => { expect(mediator.store).toBeDefined(); expect(mediator.service).toBeDefined(); - expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.options).toEqual({ endpoint: 'jobs/40291672.json' }); expect(mediator.state.isLoading).toEqual(false); }); describe('request and store data', () => { - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(job), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); + mock.onGet().reply(200, job, {}); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + mock.restore(); }); it('should store received data', (done) => { mediator.fetchJob(); - setTimeout(() => { expect(mediator.store.state.job).toEqual(job); done(); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index c57f44dae17..50a5e4ff056 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ -/* global NewBranchForm */ -import '~/new_branch_form'; +import NewBranchForm from '~/new_branch_form'; (function() { describe('Branch', function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 928a4b461cc..63abac222c4 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -4,8 +4,9 @@ import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; -import '~/render_gfm'; import '~/render_math'; +import '~/render_mermaid'; +import '~/render_gfm'; import '~/notes'; (function() { diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 0795d0aaa82..1ad7c2d8efa 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -202,7 +202,6 @@ export default { "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", "email_patches_path": "/root/acets-app/merge_requests/22.patch", "plain_diff_path": "/root/acets-app/merge_requests/22.diff", - "ci_status_path": "/root/acets-app/merge_requests/22/ci_status", "status_path": "/root/acets-app/merge_requests/22.json", "merge_check_path": "/root/acets-app/merge_requests/22/merge_check", "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status", diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js index 2cf4d8e00ed..24484796bf1 100644 --- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js +++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js @@ -16,7 +16,7 @@ describe('Issue Warning Component', () => { isLocked: true, }); - expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock'); + expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.'); }); }); @@ -27,7 +27,7 @@ describe('Issue Warning Component', () => { isConfidential: true, }); - expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash'); + expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); }); }); @@ -39,7 +39,7 @@ describe('Issue Warning Component', () => { isConfidential: true, }); - expect(vm.$el.querySelector('i')).toBeFalsy(); + expect(vm.$el.querySelector('.icon')).toBeFalsy(); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.'); }); }); diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js index 97c8a08fcdd..49bf8ee6f7c 100644 --- a/spec/javascripts/vue_shared/components/loading_button_spec.js +++ b/spec/javascripts/vue_shared/components/loading_button_spec.js @@ -66,6 +66,23 @@ describe('LoadingButton', function () { }); }); + describe('container class', () => { + it('should default to btn btn-align-content', () => { + vm = mountComponent(LoadingButton, {}); + expect(vm.$el.classList.contains('btn')).toEqual(true); + expect(vm.$el.classList.contains('btn-align-content')).toEqual(true); + }); + + it('should be configurable through props', () => { + vm = mountComponent(LoadingButton, { + containerClass: 'test-class', + }); + expect(vm.$el.classList.contains('btn')).toEqual(false); + expect(vm.$el.classList.contains('btn-align-content')).toEqual(false); + expect(vm.$el.classList.contains('test-class')).toEqual(true); + }); + }); + describe('click callback prop', () => { it('calls given callback when normal', () => { vm = mountComponent(LoadingButton, { @@ -81,7 +98,6 @@ describe('LoadingButton', function () { it('does not call given callback when disabled because of loading', () => { vm = mountComponent(LoadingButton, { loading: true, - indeterminate: true, }); spyOn(vm, '$emit'); diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js new file mode 100644 index 00000000000..818ef0af3c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import toolbar from '~/vue_shared/components/markdown/toolbar.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('toolbar', () => { + let vm; + const Toolbar = Vue.extend(toolbar); + const props = { + markdownDocsPath: '', + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('user can attach file', () => { + beforeEach(() => { + vm = mountComponent(Toolbar, props); + }); + + it('should render uploading-container', () => { + expect(vm.$el.querySelector('.uploading-container')).not.toBeNull(); + }); + }); + + describe('user cannot attach file', () => { + beforeEach(() => { + vm = mountComponent(Toolbar, Object.assign({}, props, { + canAttachFile: false, + })); + }); + + it('should not render uploading-container', () => { + expect(vm.$el.querySelector('.uploading-container')).toBeNull(); + }); + }); +}); diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb new file mode 100644 index 00000000000..532d25e121d --- /dev/null +++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Banzai::Filter::MermaidFilter do + include FilterSpecHelper + + it 'adds `js-render-mermaid` class to the `pre` tag' do + doc = filter("<pre class='code highlight js-syntax-highlight mermaid' lang='mermaid' v-pre='true'><code>graph TD;\n A-->B;\n</code></pre>") + result = doc.xpath('descendant-or-self::pre').first + + expect(result[:class]).to include('js-render-mermaid') + end +end diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 5a23e0e70cc..9f2efa05a01 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter do it "highlights as plaintext" do result = filter('<pre><code lang="ruby">This is a test</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code>This is a test</code></pre>') end end end diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb index 84cacdd3f0d..010deae822c 100644 --- a/spec/lib/container_registry/path_spec.rb +++ b/spec/lib/container_registry/path_spec.rb @@ -86,6 +86,24 @@ describe ContainerRegistry::Path do it { is_expected.to be_valid } end + + context 'when path contains double underscore' do + let(:path) { 'my/repository__name' } + + it { is_expected.to be_valid } + end + + context 'when path contains invalid separator with dot' do + let(:path) { 'some/registry-.name' } + + it { is_expected.not_to be_valid } + end + + context 'when path contains invalid separator with underscore' do + let(:path) { 'some/registry._name' } + + it { is_expected.not_to be_valid } + end end describe '#has_repository?' do diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb new file mode 100644 index 00000000000..ffcd90b9fcb --- /dev/null +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Gitlab::Auth::RequestAuthenticator do + let(:env) do + { + 'rack.input' => '', + 'REQUEST_METHOD' => 'GET' + } + end + let(:request) { ActionDispatch::Request.new(env) } + + subject { described_class.new(request) } + + describe '#user' do + let!(:sessionless_user) { build(:user) } + let!(:session_user) { build(:user) } + + it 'returns sessionless user first' do + allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user) + allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) + + expect(subject.user).to eq sessionless_user + end + + it 'returns session user if no sessionless user found' do + allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) + + expect(subject.user).to eq session_user + end + + it 'returns nil if no user found' do + expect(subject.user).to be_blank + end + + it 'bubbles up exceptions' do + allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_raise(Gitlab::Auth::UnauthorizedError) + end + end + + describe '#find_sessionless_user' do + let!(:access_token_user) { build(:user) } + let!(:rss_token_user) { build(:user) } + + it 'returns access_token user first' do + allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user) + + expect(subject.find_sessionless_user).to eq access_token_user + end + + it 'returns rss_token user if no access_token user found' do + allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user) + + expect(subject.find_sessionless_user).to eq rss_token_user + end + + it 'returns nil if no user found' do + expect(subject.find_sessionless_user).to be_blank + end + + it 'rescue Gitlab::Auth::AuthenticationError exceptions' do + allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError) + + expect(subject.find_sessionless_user).to be_blank + end + end +end diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb new file mode 100644 index 00000000000..4637816570c --- /dev/null +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -0,0 +1,194 @@ +require 'spec_helper' + +describe Gitlab::Auth::UserAuthFinders do + include described_class + + let(:user) { create(:user) } + let(:env) do + { + 'rack.input' => '' + } + end + let(:request) { Rack::Request.new(env)} + + def set_param(key, value) + request.update_param(key, value) + end + + describe '#find_user_from_warden' do + context 'with CSRF token' do + before do + allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true) + end + + context 'with invalid credentials' do + it 'returns nil' do + expect(find_user_from_warden).to be_nil + end + end + + context 'with valid credentials' do + it 'returns the user' do + env['warden'] = double("warden", authenticate: user) + + expect(find_user_from_warden).to eq user + end + end + end + + context 'without CSRF token' do + it 'returns nil' do + allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false) + env['warden'] = double("warden", authenticate: user) + + expect(find_user_from_warden).to be_nil + end + end + end + + describe '#find_user_from_rss_token' do + context 'when the request format is atom' do + before do + env['HTTP_ACCEPT'] = 'application/atom+xml' + end + + it 'returns user if valid rss_token' do + set_param(:rss_token, user.rss_token) + + expect(find_user_from_rss_token).to eq user + end + + it 'returns nil if rss_token is blank' do + expect(find_user_from_rss_token).to be_nil + end + + it 'returns exception if invalid rss_token' do + set_param(:rss_token, 'invalid_token') + + expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + context 'when the request format is not atom' do + it 'returns nil' do + set_param(:rss_token, user.rss_token) + + expect(find_user_from_rss_token).to be_nil + end + end + end + + describe '#find_user_from_access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'returns nil if no access_token present' do + expect(find_personal_access_token).to be_nil + end + + context 'when validate_access_token! returns valid' do + it 'returns user' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + + expect(find_user_from_access_token).to eq user + end + + it 'returns exception if token has no user' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil) + + expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + end + + describe '#find_personal_access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + context 'passed as header' do + it 'returns token if valid personal_access_token' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + + expect(find_personal_access_token).to eq personal_access_token + end + end + + context 'passed as param' do + it 'returns token if valid personal_access_token' do + set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, personal_access_token.token) + + expect(find_personal_access_token).to eq personal_access_token + end + end + + it 'returns nil if no personal_access_token' do + expect(find_personal_access_token).to be_nil + end + + it 'returns exception if invalid personal_access_token' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid_token' + + expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + describe '#find_oauth_access_token' do + let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } + + context 'passed as header' do + it 'returns token if valid oauth_access_token' do + env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}" + + expect(find_oauth_access_token.token).to eq token.token + end + end + + context 'passed as param' do + it 'returns user if valid oauth_access_token' do + set_param(:access_token, token.token) + + expect(find_oauth_access_token.token).to eq token.token + end + end + + it 'returns nil if no oauth_access_token' do + expect(find_oauth_access_token).to be_nil + end + + it 'returns exception if invalid oauth_access_token' do + env['HTTP_AUTHORIZATION'] = "Bearer invalid_token" + + expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + describe '#validate_access_token!' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'returns nil if no access_token present' do + expect(validate_access_token!).to be_nil + end + + context 'token is not valid' do + before do + allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token) + end + + it 'returns Gitlab::Auth::ExpiredError if token expired' do + personal_access_token.expires_at = 1.day.ago + + expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError) + end + + it 'returns Gitlab::Auth::RevokedError if token revoked' do + personal_access_token.revoke! + + expect { validate_access_token! }.to raise_error(Gitlab::Auth::RevokedError) + end + + it 'returns Gitlab::Auth::InsufficientScopeError if invalid token scope' do + expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index 2c2684a6fc9..994992f79d4 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -3,12 +3,9 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do let(:migration) { described_class.new } let(:base1) { create(:project) } - let(:base1_fork1) { create(:project) } - let(:base1_fork2) { create(:project) } let(:base2) { create(:project) } let(:base2_fork1) { create(:project) } - let(:base2_fork2) { create(:project) } let!(:forked_project_links) { table(:forked_project_links) } let!(:fork_networks) { table(:fork_networks) } @@ -21,21 +18,24 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch # A normal fork link forked_project_links.create(id: 1, forked_from_project_id: base1.id, - forked_to_project_id: base1_fork1.id) + forked_to_project_id: create(:project).id) forked_project_links.create(id: 2, forked_from_project_id: base1.id, - forked_to_project_id: base1_fork2.id) - + forked_to_project_id: create(:project).id) forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) + + # create a fork of a fork forked_project_links.create(id: 4, forked_from_project_id: base2_fork1.id, forked_to_project_id: create(:project).id) - forked_project_links.create(id: 5, - forked_from_project_id: base2.id, - forked_to_project_id: base2_fork2.id) + forked_from_project_id: create(:project).id, + forked_to_project_id: create(:project).id) + + # Stub out the calls to the other migrations + allow(BackgroundMigrationWorker).to receive(:perform_in) migration.perform(1, 3) end @@ -80,11 +80,11 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch end it 'only processes a single batch of links at a time' do - expect(fork_network_members.count).to eq(5) + expect(fork_networks.count).to eq(2) migration.perform(3, 5) - expect(fork_network_members.count).to eq(7) + expect(fork_networks.count).to eq(3) end it 'can be repeated without effect' do diff --git a/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb index 4ea7f441f7c..0cb753c5853 100644 --- a/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20171026082505_populate_merge_requests_latest_merge_request_diff_id') -describe PopulateMergeRequestsLatestMergeRequestDiffId, :migration do +describe Gitlab::BackgroundMigration::PopulateMergeRequestsLatestMergeRequestDiffId, :migration, schema: 20171026082505 do let(:projects_table) { table(:projects) } let(:merge_requests_table) { table(:merge_requests) } let(:merge_request_diffs_table) { table(:merge_request_diffs) } @@ -27,30 +26,32 @@ describe PopulateMergeRequestsLatestMergeRequestDiffId, :migration do merge_request_diffs_table.where(merge_request_id: merge_request.id) end - describe '#up' do + describe '#perform' do it 'ignores MRs without diffs' do merge_request_without_diff = create_mr!('without_diff') + mr_id = merge_request_without_diff.id expect(merge_request_without_diff.latest_merge_request_diff_id).to be_nil - expect { migrate! } + expect { subject.perform(mr_id, mr_id) } .not_to change { merge_request_without_diff.reload.latest_merge_request_diff_id } end it 'ignores MRs that have a diff ID already set' do merge_request_with_multiple_diffs = create_mr!('with_multiple_diffs', diffs: 3) diff_id = diffs_for(merge_request_with_multiple_diffs).minimum(:id) + mr_id = merge_request_with_multiple_diffs.id merge_request_with_multiple_diffs.update!(latest_merge_request_diff_id: diff_id) - expect { migrate! } + expect { subject.perform(mr_id, mr_id) } .not_to change { merge_request_with_multiple_diffs.reload.latest_merge_request_diff_id } end it 'migrates multiple MR diffs to the correct values' do merge_requests = Array.new(3).map.with_index { |_, i| create_mr!(i, diffs: 3) } - migrate! + subject.perform(merge_requests.first.id, merge_requests.last.id) merge_requests.each do |merge_request| expect(merge_request.reload.latest_merge_request_diff_id) diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index a66347ead76..a6a1d9e619f 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do create( :project, import_source: project_identifier, + import_url: "https://bitbucket.org/#{project_identifier}.git", import_data_attributes: { credentials: data } ) end let(:importer) { described_class.new(project) } + let(:gitlab_shell) { double } let(:issues_statuses_sample_data) do { @@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do } end + before do + allow(importer).to receive(:gitlab_shell) { gitlab_shell } + end + context 'issues statuses' do before do # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this @@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do end it 'maps statuses to open or closed' do + allow(importer).to receive(:import_wiki) + importer.execute expect(project.issues.where(state: "closed").size).to eq(5) expect(project.issues.where(state: "opened").size).to eq(2) end - it 'calls import_wiki' do - expect(importer).to receive(:import_wiki) - importer.execute + describe 'wiki import' do + it 'is skipped when the wiki exists' do + expect(project.wiki).to receive(:repository_exists?) { true } + expect(importer.gitlab_shell).not_to receive(:import_repository) + + importer.execute + + expect(importer.errors).to be_empty + end + + it 'imports to the project disk_path' do + expect(project.wiki).to receive(:repository_exists?) { false } + expect(importer.gitlab_shell).to receive(:import_repository).with( + project.repository_storage_path, + project.wiki.disk_path, + project.import_url + '/wiki' + ) + + importer.execute + + expect(importer.errors).to be_empty + end end end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index bf981d2f6f6..92792144429 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -84,6 +84,13 @@ describe Gitlab::Conflict::File do expect(line.text).to eq(html_to_text(line.rich_text)) end end + + # This spec will break if Rouge's highlighting changes, but we need to + # ensure that the lines are actually highlighted. + it 'highlights the lines correctly' do + expect(conflict_file.lines.first.rich_text) + .to eq("<span id=\"LC1\" class=\"line\" lang=\"ruby\"><span class=\"k\">module</span> <span class=\"nn\">Gitlab</span></span>\n") + end end describe '#sections' do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index 8922370b0a0..e850b5cd6a4 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end + it 'does not move the repositories when hashed storage is enabled' do + project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) + + expect(subject).not_to receive(:move_repository) + + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') + end + it 'moves uploads' do expect(subject).to receive(:move_uploads) .with('known-parent/the-path', 'known-parent/the-path0') @@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end + it 'does not move uploads when hashed storage is enabled for attachments' do + project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments]) + + expect(subject).not_to receive(:move_uploads) + + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') + end + it 'moves pages' do expect(subject).to receive(:move_pages) .with('known-parent/the-path', 'known-parent/the-path0') diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index c91895cedc3..ff9acfd08b9 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -116,12 +116,8 @@ describe Gitlab::Diff::File do end context 'when renamed' do - let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') } - let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') } - - before do - allow(diff_file.new_blob).to receive(:id).and_return(diff_file.old_blob.id) - end + let(:commit) { project.commit('94bb47ca1297b7b3731ff2a36923640991e9236f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('CHANGELOG.md') } it 'returns false' do expect(diff_file.content_changed?).to be_falsey diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index ee657101f4c..65edc750f39 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -487,6 +487,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do loop do break if @count.zero? + # It is critical to decrement before yielding. We may never reach the lines after 'yield'. @count -= 1 yield @value diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5d990b42c24..f0da77c61bb 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -629,16 +629,29 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#remote_tags' do + let(:remote_name) { 'upstream' } let(:target_commit_id) { SeedRepo::Commit::ID } + let(:user) { create(:user) } + let(:tag_name) { 'v0.0.1' } + let(:tag_message) { 'My tag' } + let(:remote_repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end - subject { repository.remote_tags('upstream') } + subject { repository.remote_tags(remote_name) } - it 'gets the remote tags' do - expect(repository).to receive(:list_remote_tags).with('upstream') - .and_return(["#{target_commit_id}\trefs/tags/v0.0.1\n"]) + before do + repository.add_remote(remote_name, remote_repository.path) + remote_repository.add_tag(tag_name, user: user, target: target_commit_id) + end + + after do + ensure_seeds + end + it 'gets the remote tags' do expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag) - expect(subject.first.name).to eq('v0.0.1') + expect(subject.first.name).to eq(tag_name) expect(subject.first.dereferenced_target.id).to eq(target_commit_id) end end @@ -1770,6 +1783,32 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#delete_all_refs_except' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + + before do + repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") + repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1") + repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae") + repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") + end + + after do + ensure_seeds + end + + it 'deletes all refs except those with the specified prefixes' do + repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads)) + expect(repository.ref_exists?("refs/delete/a")).to be(false) + expect(repository.ref_exists?("refs/also-delete/b")).to be(false) + expect(repository.ref_exists?("refs/keep/c")).to be(true) + expect(repository.ref_exists?("refs/also-keep/d")).to be(true) + expect(repository.ref_exists?("refs/heads/master")).to be(true) + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 8127b4842b7..951e146a30a 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError) end end + + describe '#delete_refs' do + let(:prefixes) { %w(refs/heads refs/keep-around) } + + it 'sends a delete_refs message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:delete_refs) + .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash)) + .and_return(double('delete_refs_response')) + + client.delete_refs(except_with_prefixes: prefixes) + end + end end 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 e4b4cf5ba85..c2bda6f8821 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -155,7 +155,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end it 'has no source if source/target differ' do - expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) + expect(MergeRequest.find_by_title('MR2').source_project_id).to be_nil end end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 67121937398..60a134be939 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -127,6 +127,14 @@ describe Gitlab::Middleware::Go do include_examples 'go-get=1', enabled_protocol: nil end + + context 'with nothing disabled (blank string)' do + before do + stub_application_setting(enabled_git_access_protocol: '') + end + + include_examples 'go-get=1', enabled_protocol: nil + end end def go diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index b14735943a5..07ba11b93a3 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -84,14 +84,23 @@ describe Gitlab::Middleware::ReadOnly do end it 'expects POST of new file that looks like an LFS batch url to be disallowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') expect(response).to be_a_redirect expect(subject).to disallow_request end + it 'returns last_vistited_url for disallowed request' do + response = request.post('/test_request') + + expect(response.location).to eq 'http://localhost/' + end + context 'whitelisted requests' do it 'expects a POST internal request to be allowed' do + expect(Rails.application.routes).not_to receive(:recognize_path) + response = request.post("/api/#{API::API.version}/internal") expect(response).not_to be_a_redirect @@ -99,6 +108,7 @@ describe Gitlab::Middleware::ReadOnly do end it 'expects a POST LFS request to batch URL to be allowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/rouge.git/info/lfs/objects/batch') expect(response).not_to be_a_redirect @@ -106,6 +116,7 @@ describe Gitlab::Middleware::ReadOnly do end it 'expects a POST request to git-upload-pack URL to be allowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/rouge.git/git-upload-pack') expect(response).not_to be_a_redirect diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index c7471a21fda..2f19fb7312d 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -662,4 +662,13 @@ describe Gitlab::OAuth::User do end end end + + describe '.find_by_uid_and_provider' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + it 'normalizes extern_uid' do + allow(oauth_user.auth_hash).to receive(:uid).and_return('MY-UID') + expect(oauth_user.find_user).to eql gl_user + end + end end diff --git a/spec/lib/milestone_array_spec.rb b/spec/lib/milestone_array_spec.rb new file mode 100644 index 00000000000..df91677b925 --- /dev/null +++ b/spec/lib/milestone_array_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe MilestoneArray do + let(:object1) { instance_double("BirdMilestone", due_date: Time.now, start_date: Time.now - 15.days, title: 'v2.0') } + let(:object2) { instance_double("CatMilestone", due_date: Time.now - 1.day, start_date: nil, title: 'v1.0') } + let(:object3) { instance_double("DogMilestone", due_date: nil, start_date: Time.now - 30.days, title: 'v3.0') } + let(:array) { [object1, object3, object2] } + + describe '#sort' do + it 'reorders array with due date in ascending order with nulls last' do + expect(described_class.sort(array, 'due_date_asc')).to eq([object2, object1, object3]) + end + + it 'reorders array with due date in desc order with nulls last' do + expect(described_class.sort(array, 'due_date_desc')).to eq([object1, object2, object3]) + end + + it 'reorders array with start date in ascending order with nulls last' do + expect(described_class.sort(array, 'start_date_asc')).to eq([object3, object1, object2]) + end + + it 'reorders array with start date in descending order with nulls last' do + expect(described_class.sort(array, 'start_date_desc')).to eq([object1, object3, object2]) + end + + it 'reorders array with title in ascending order' do + expect(described_class.sort(array, 'name_asc')).to eq([object2, object1, object3]) + end + + it 'reorders array with title in descending order' do + expect(described_class.sort(array, 'name_desc')).to eq([object3, object1, object2]) + end + end +end diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb new file mode 100644 index 00000000000..cf6ae5cda74 --- /dev/null +++ b/spec/migrations/remove_empty_fork_networks_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171114104051_remove_empty_fork_networks.rb') + +describe RemoveEmptyForkNetworks, :migration do + let!(:fork_networks) { table(:fork_networks) } + + let(:deleted_project) { create(:project) } + let!(:empty_network) { create(:fork_network, id: 1, root_project_id: deleted_project.id) } + let!(:other_network) { create(:fork_network, id: 2, root_project_id: create(:project).id) } + + before do + deleted_project.destroy! + end + + it 'deletes only the fork network without members' do + expect(fork_networks.count).to eq(2) + + migrate! + + expect(fork_networks.find_by(id: empty_network.id)).to be_nil + expect(fork_networks.find_by(id: other_network.id)).not_to be_nil + expect(fork_networks.count).to eq(1) + end +end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb index f95bd6e3511..76afb6c19cf 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb @@ -2,19 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170703130158_schedule_merge_request_diff_migrations') describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do - matcher :be_scheduled_migration do |time, *expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] && - job['at'].to_i == time.to_i - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - let(:merge_request_diffs) { table(:merge_request_diffs) } let(:merge_requests) { table(:merge_requests) } let(:projects) { table(:projects) } @@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes.from_now, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes.from_now, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb index 4ab1bb67058..cf323973384 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb @@ -2,19 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170926150348_schedule_merge_request_diff_migrations_take_two') describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do - matcher :be_scheduled_migration do |time, *expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] && - job['at'].to_i == time.to_i - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - let(:merge_request_diffs) { table(:merge_request_diffs) } let(:merge_requests) { table(:merge_requests) } let(:projects) { table(:projects) } @@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes.from_now, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes.from_now, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb new file mode 100644 index 00000000000..158d0bc02ed --- /dev/null +++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations') + +describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :sidekiq do + let(:projects_table) { table(:projects) } + let(:merge_requests_table) { table(:merge_requests) } + let(:merge_request_diffs_table) { table(:merge_request_diffs) } + + let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') } + + let!(:merge_request_1) { create_mr!('mr_1', diffs: 1) } + let!(:merge_request_2) { create_mr!('mr_2', diffs: 2) } + let!(:merge_request_migrated) { create_mr!('merge_request_migrated', diffs: 3) } + let!(:merge_request_4) { create_mr!('mr_4', diffs: 3) } + + def create_mr!(name, diffs: 0) + merge_request = + merge_requests_table.create!(target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: name, + title: name) + + diffs.times do + merge_request_diffs_table.create!(merge_request_id: merge_request.id) + end + + merge_request + end + + def diffs_for(merge_request) + merge_request_diffs_table.where(merge_request_id: merge_request.id) + end + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + diff_id = diffs_for(merge_request_migrated).minimum(:id) + merge_request_migrated.update!(latest_merge_request_diff_id: diff_id) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id) + expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'schedules background migrations' do + Sidekiq::Testing.inline! do + expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 3 + + migrate! + + expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 0 + end + end +end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 47342f98283..81e35e6c931 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -16,6 +16,23 @@ describe Blob do end end + describe '.lazy' do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit_by(oid: 'e63f41fe459e62e1228fcef60d7189127aeba95a') } + + it 'fetches all blobs when the first is accessed' do + changelog = described_class.lazy(project, commit.id, 'CHANGELOG') + contributing = described_class.lazy(project, commit.id, 'CONTRIBUTING.md') + + expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original + expect(Gitlab::Git::Blob).not_to receive(:find) + + # Access property so the values are loaded + changelog.id + contributing.id + end + end + describe '#data' do context 'using a binary blob' do it 'returns the data as-is' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 2c9e7013b77..3a19a0753e2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -625,38 +625,29 @@ describe Ci::Pipeline, :mailer do shared_context 'with some outdated pipelines' do before do - create_pipeline(:canceled, 'ref', 'A') - create_pipeline(:success, 'ref', 'A') - create_pipeline(:failed, 'ref', 'B') - create_pipeline(:skipped, 'feature', 'C') + create_pipeline(:canceled, 'ref', 'A', project) + create_pipeline(:success, 'ref', 'A', project) + create_pipeline(:failed, 'ref', 'B', project) + create_pipeline(:skipped, 'feature', 'C', project) end - def create_pipeline(status, ref, sha) - create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + def create_pipeline(status, ref, sha, project) + create( + :ci_empty_pipeline, + status: status, + ref: ref, + sha: sha, + project: project + ) end end - describe '.latest' do + describe '.newest_first' do include_context 'with some outdated pipelines' - context 'when no ref is specified' do - let(:pipelines) { described_class.latest.all } - - it 'returns the latest pipeline for the same ref and different sha' do - expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') - expect(pipelines.map(&:status)) - .to contain_exactly('success', 'failed', 'skipped') - end - end - - context 'when ref is specified' do - let(:pipelines) { described_class.latest('ref').all } - - it 'returns the latest pipeline for ref and different sha' do - expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') - expect(pipelines.map(&:status)) - .to contain_exactly('success', 'failed') - end + it 'returns the pipelines from new to old' do + expect(described_class.newest_first.pluck(:status)) + .to eq(%w[skipped failed success canceled]) end end @@ -664,20 +655,14 @@ describe Ci::Pipeline, :mailer do include_context 'with some outdated pipelines' context 'when no ref is specified' do - let(:latest_status) { described_class.latest_status } - - it 'returns the latest status for the same ref and different sha' do - expect(latest_status).to eq(described_class.latest.status) - expect(latest_status).to eq('failed') + it 'returns the status of the latest pipeline' do + expect(described_class.latest_status).to eq('skipped') end end context 'when ref is specified' do - let(:latest_status) { described_class.latest_status('ref') } - - it 'returns the latest status for ref and different sha' do - expect(latest_status).to eq(described_class.latest_status('ref')) - expect(latest_status).to eq('failed') + it 'returns the status of the latest pipeline for the given ref' do + expect(described_class.latest_status('ref')).to eq('failed') end end end @@ -686,7 +671,7 @@ describe Ci::Pipeline, :mailer do include_context 'with some outdated pipelines' let!(:latest_successful_pipeline) do - create_pipeline(:success, 'ref', 'D') + create_pipeline(:success, 'ref', 'D', project) end it 'returns the latest successful pipeline' do @@ -698,8 +683,13 @@ describe Ci::Pipeline, :mailer do describe '.latest_successful_for_refs' do include_context 'with some outdated pipelines' - let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') } - let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') } + let!(:latest_successful_pipeline1) do + create_pipeline(:success, 'ref1', 'D', project) + end + + let!(:latest_successful_pipeline2) do + create_pipeline(:success, 'ref2', 'D', project) + end it 'returns the latest successful pipeline for both refs' do refs = %w(ref1 ref2 ref3) @@ -708,6 +698,62 @@ describe Ci::Pipeline, :mailer do end end + describe '.latest_status_per_commit' do + let(:project) { create(:project) } + + before do + pairs = [ + %w[success ref1 123], + %w[manual master 123], + %w[failed ref 456] + ] + + pairs.each do |(status, ref, sha)| + create( + :ci_empty_pipeline, + status: status, + ref: ref, + sha: sha, + project: project + ) + end + end + + context 'without a ref' do + it 'returns a Hash containing the latest status per commit for all refs' do + expect(described_class.latest_status_per_commit(%w[123 456])) + .to eq({ '123' => 'manual', '456' => 'failed' }) + end + + it 'only includes the status of the given commit SHAs' do + expect(described_class.latest_status_per_commit(%w[123])) + .to eq({ '123' => 'manual' }) + end + + context 'when there are two pipelines for a ref and SHA' do + it 'returns the status of the latest pipeline' do + create( + :ci_empty_pipeline, + status: 'failed', + ref: 'master', + sha: '123', + project: project + ) + + expect(described_class.latest_status_per_commit(%w[123])) + .to eq({ '123' => 'failed' }) + end + end + end + + context 'with a ref' do + it 'only includes the pipelines for the given ref' do + expect(described_class.latest_status_per_commit(%w[123 456], 'master')) + .to eq({ '123' => 'manual' }) + end + end + end + describe '.internal_sources' do subject { described_class.internal_sources } @@ -1456,6 +1502,10 @@ describe Ci::Pipeline, :mailer do create(:ci_build, :success, :artifacts, pipeline: pipeline) end + it 'returns an Array' do + expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array) + end + it 'returns the latest builds' do expect(pipeline.latest_builds_with_artifacts).to eq([build]) end diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb new file mode 100644 index 00000000000..066fe7d154e --- /dev/null +++ b/spec/models/commit_collection_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe CommitCollection do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit } + + describe '#each' do + it 'yields every commit' do + collection = described_class.new(project, [commit]) + + expect { |b| collection.each(&b) }.to yield_with_args(commit) + end + end + + describe '#with_pipeline_status' do + it 'sets the pipeline status for every commit so no additional queries are necessary' do + create( + :ci_empty_pipeline, + ref: 'master', + sha: commit.id, + status: 'success', + project: project + ) + + collection = described_class.new(project, [commit]) + collection.with_pipeline_status + + recorder = ActiveRecord::QueryRecorder.new do + expect(commit.status).to eq('success') + end + + expect(recorder.count).to be_zero + end + end + + describe '#respond_to_missing?' do + it 'returns true when the underlying Array responds to the message' do + collection = described_class.new(project, []) + + expect(collection.respond_to?(:last)).to eq(true) + end + + it 'returns false when the underlying Array does not respond to the message' do + collection = described_class.new(project, []) + + expect(collection.respond_to?(:foo)).to eq(false) + end + end + + describe '#method_missing' do + it 'delegates undefined methods to the underlying Array' do + collection = described_class.new(project, [commit]) + + expect(collection.length).to eq(1) + expect(collection.last).to eq(commit) + expect(collection).not_to be_empty + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e3cfa149e3a..d18a5c9dfa6 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -351,12 +351,19 @@ eos end it 'gives compound status from latest pipelines if ref is nil' do - expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) - expect(commit.status(nil)).to eq('failed') + expect(commit.status(nil)).to eq(pipeline_from_fix.status) end end end + describe '#set_status_for_ref' do + it 'sets the status for a given reference' do + commit.set_status_for_ref('master', 'failed') + + expect(commit.status('master')).to eq('failed') + end + end + describe '#participants' do let(:user1) { build(:user) } let(:user2) { build(:user) } diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb index b26de3f3b97..c90b32c5d77 100644 --- a/spec/models/diff_viewer/base_spec.rb +++ b/spec/models/diff_viewer/base_spec.rb @@ -32,10 +32,8 @@ describe DiffViewer::Base do end context 'when the binaryness does not match' do - before do - allow(diff_file.old_blob).to receive(:binary?).and_return(false) - allow(diff_file.new_blob).to receive(:binary?).and_return(false) - end + let(:commit) { project.commit_by(oid: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('Gemfile.zip') } it 'returns false' do expect(viewer_class.can_render?(diff_file)).to be_falsey @@ -60,8 +58,7 @@ describe DiffViewer::Base do context 'when the binaryness does not match' do before do - allow(diff_file.old_blob).to receive(:binary?).and_return(true) - allow(diff_file.new_blob).to receive(:binary?).and_return(true) + allow_any_instance_of(Blob).to receive(:binary?).and_return(true) end it 'returns false' do @@ -77,12 +74,12 @@ describe DiffViewer::Base do end context 'when the file was renamed and only the old blob is supported' do - let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:commit) { project.commit_by(oid: '2f63565e7aac07bcdadb654e253078b727143ec4') } let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } before do allow(diff_file).to receive(:renamed_file?).and_return(true) - allow(diff_file.new_blob).to receive(:extension).and_return('jpeg') + viewer_class.extensions = %w(notjpg) end it 'returns false' do @@ -94,8 +91,7 @@ describe DiffViewer::Base do describe '#collapsed?' do context 'when the combined blob size is larger than the collapse limit' do before do - allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes) - allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes) + allow(diff_file).to receive(:raw_size).and_return(1025.kilobytes) end it 'returns true' do @@ -113,8 +109,7 @@ describe DiffViewer::Base do describe '#too_large?' do context 'when the combined blob size is larger than the size limit' do before do - allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) - allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + allow(diff_file).to receive(:raw_size).and_return(6.megabytes) end it 'returns true' do @@ -132,8 +127,7 @@ describe DiffViewer::Base do describe '#render_error' do context 'when the combined blob size is larger than the size limit' do before do - allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) - allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + allow(diff_file).to receive(:raw_size).and_return(6.megabytes) end it 'returns :too_large' do diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb index 92e613f92de..98a8f6d4cc9 100644 --- a/spec/models/diff_viewer/server_side_spec.rb +++ b/spec/models/diff_viewer/server_side_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe DiffViewer::ServerSide do - let(:project) { create(:project, :repository) } - let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } - let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + set(:project) { create(:project, :repository) } + let(:commit) { project.commit_by(oid: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let!(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } let(:viewer_class) do Class.new(DiffViewer::Base) do @@ -15,8 +15,7 @@ describe DiffViewer::ServerSide do describe '#prepare!' do it 'loads all diff file data' do - expect(diff_file.old_blob).to receive(:load_all_data!) - expect(diff_file.new_blob).to receive(:load_all_data!) + expect(Blob).to receive(:lazy).at_least(:twice) subject.prepare! end diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb index 532ca1fca8c..25bf596fddc 100644 --- a/spec/models/fork_network_member_spec.rb +++ b/spec/models/fork_network_member_spec.rb @@ -5,4 +5,22 @@ describe ForkNetworkMember do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:fork_network) } end + + describe 'destroying a ForkNetworkMember' do + let(:fork_network_member) { create(:fork_network_member) } + let(:fork_network) { fork_network_member.fork_network } + + it 'removes the fork network if it was the last member' do + fork_network.fork_network_members.destroy_all + + expect(ForkNetwork.count).to eq(0) + end + + it 'does not destroy the fork network if there are members left' do + fork_network_member.destroy! + + # The root of the fork network is left + expect(ForkNetwork.count).to eq(1) + end + end end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 3ed048744de..a45a6088831 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -33,5 +33,15 @@ describe Identity do expect(identity).to eq(ldap_identity) end end + + context 'any other provider' do + let!(:test_entity) { create(:identity, provider: 'test_provider', extern_uid: 'test_uid') } + + it 'the extern_uid lookup is case insensitive' do + identity = described_class.with_extern_uid('test_provider', 'TEST_UID').first + + expect(identity).to eq(test_entity) + end + end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 81c2057e175..4cd9e3f4f1d 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -166,4 +166,27 @@ describe Key, :mailer do expect(key.public_key.key_text).to eq(valid_key) end end + + describe '#refresh_user_cache', :use_clean_rails_memory_store_caching do + context 'when the key belongs to a user' do + it 'refreshes the keys count cache for the user' do + expect_any_instance_of(Users::KeysCountService) + .to receive(:refresh_cache) + .and_call_original + + key = create(:personal_key) + + expect(Users::KeysCountService.new(key.user).count).to eq(1) + end + end + + context 'when the key does not belong to a user' do + it 'does nothing' do + expect_any_instance_of(Users::KeysCountService) + .not_to receive(:refresh_cache) + + create(:key) + end + end + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 13e37fffa4e..47f4a792e5c 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -11,7 +11,7 @@ describe Milestone do milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday) expect(milestone).not_to be_valid - expect(milestone.errors[:start_date]).to include("Can't be greater than due date") + expect(milestone.errors[:due_date]).to include("must be greater than start date") end end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 1ecb50586c7..6e7e8c4c570 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -231,6 +231,37 @@ describe Note do end end + describe '#cross_reference?' do + it 'falsey for user-generated notes' do + note = create(:note, system: false) + + expect(note.cross_reference?).to be_falsy + end + + context 'when the note might contain cross references' do + SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.each do |type| + let(:note) { create(:note, :system) } + let!(:metadata) { create(:system_note_metadata, note: note, action: type) } + + it 'delegates to the cross-reference regex' do + expect(note).to receive(:matches_cross_reference_regex?).and_return(false) + + note.cross_reference? + end + end + end + + context 'when the note cannot contain cross references' do + let(:commit_note) { build(:note, note: 'mentioned in 1312312313 something else.', system: true) } + let(:label_note) { build(:note, note: 'added ~2323232323', system: true) } + + it 'scan for a `mentioned in` prefix' do + expect(commit_note.cross_reference?).to be_truthy + expect(label_note.cross_reference?).to be_falsy + end + end + end + describe 'clear_blank_line_code!' do it 'clears a blank line code before validation' do note = build(:note, line_code: ' ') diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index 5e8e880985e..fabcb142858 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -46,6 +46,7 @@ describe FlowdockService do @sample_data[:commits].each do |commit| # One request to Flowdock per new commit next if commit[:id] == @sample_data[:before] + expect(WebMock).to have_requested(:post, @api_url).with( body: /#{commit[:id]}.*#{project.path}/ ).once diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 8a6aa767ce6..e9e6abb0d5f 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1166,6 +1166,31 @@ describe Repository do end end + describe '#branch_exists?' do + it 'uses branch_names' do + allow(repository).to receive(:branch_names).and_return(['foobar']) + + expect(repository.branch_exists?('foobar')).to eq(true) + expect(repository.branch_exists?('master')).to eq(false) + end + end + + describe '#branch_names', :use_clean_rails_memory_store_caching do + let(:fake_branch_names) { ['foobar'] } + + it 'gets cached across Repository instances' do + allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names) + + expect(repository.branch_names).to eq(fake_branch_names) + + fresh_repository = Project.find(project.id).repository + expect(fresh_repository.object_id).not_to eq(repository.object_id) + + expect(fresh_repository.raw_repository).not_to receive(:branch_names) + expect(fresh_repository.branch_names).to eq(fake_branch_names) + end + end + describe '#update_autocrlf_option' do describe 'when autocrlf is not already set to :input' do before do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 88732962071..86647ddf6ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -828,7 +828,7 @@ describe User do end end - describe '#require_ssh_key?' do + describe '#require_ssh_key?', :use_clean_rails_memory_store_caching do protocol_and_expectation = { 'http' => false, 'ssh' => true, @@ -843,6 +843,12 @@ describe User do expect(user.require_ssh_key?).to eq(expected) end end + + it 'returns false when the user has 1 or more SSH keys' do + key = create(:personal_key) + + expect(key.user.require_ssh_key?).to eq(false) + end end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index a7227b38850..ea75434e399 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -373,7 +373,7 @@ describe WikiPage do end it 'returns commit sha' do - expect(@page.last_commit_sha).to eq @page.commit.sha + expect(@page.last_commit_sha).to eq @page.last_version.sha end it 'is changed after page updated' do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 6c0996c543d..0462f494e15 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -11,7 +11,6 @@ describe API::Helpers do let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } - let(:params) { {} } let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) } let(:env) do { @@ -19,10 +18,13 @@ describe API::Helpers do 'rack.session' => { _csrf_token: csrf_token }, - 'REQUEST_METHOD' => 'GET' + 'REQUEST_METHOD' => 'GET', + 'CONTENT_TYPE' => 'text/plain;charset=utf-8' } end let(:header) { } + let(:request) { Grape::Request.new(env)} + let(:params) { request.params } before do allow_any_instance_of(self.class).to receive(:options).and_return({}) @@ -37,6 +39,10 @@ describe API::Helpers do raise Exception.new("#{status} - #{message}") end + def set_param(key, value) + request.update_param(key, value) + end + describe ".current_user" do subject { current_user } @@ -132,13 +138,13 @@ describe API::Helpers do let(:personal_access_token) { create(:personal_access_token, user: user) } it "returns a 401 response for an invalid token" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token' expect { current_user }.to raise_error /401/ end it "returns a 403 response for a user without access" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) expect { current_user }.to raise_error /403/ @@ -146,35 +152,35 @@ describe API::Helpers do it 'returns a 403 response for a user who is blocked' do user.block! - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect { current_user }.to raise_error /403/ end it "sets current_user" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) end it "does not allow tokens without the appropriate scope" do personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError + expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError end it 'does not allow revoked tokens' do personal_access_token.revoke! - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error API::APIGuard::RevokedError + expect { current_user }.to raise_error Gitlab::Auth::RevokedError end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error API::APIGuard::ExpiredError + expect { current_user }.to raise_error Gitlab::Auth::ExpiredError end end end @@ -350,7 +356,7 @@ describe API::Helpers do context 'when using param' do context 'when providing username' do before do - params[API::Helpers::SUDO_PARAM] = user.username + set_param(API::Helpers::SUDO_PARAM, user.username) end it_behaves_like 'successful sudo' @@ -358,7 +364,7 @@ describe API::Helpers do context 'when providing user ID' do before do - params[API::Helpers::SUDO_PARAM] = user.id.to_s + set_param(API::Helpers::SUDO_PARAM, user.id.to_s) end it_behaves_like 'successful sudo' @@ -368,7 +374,7 @@ describe API::Helpers do context 'when user does not exist' do before do - params[API::Helpers::SUDO_PARAM] = 'nonexistent' + set_param(API::Helpers::SUDO_PARAM, 'nonexistent') end it 'raises an error' do @@ -382,11 +388,11 @@ describe API::Helpers do token.scopes = %w[api] token.save! - params[API::Helpers::SUDO_PARAM] = user.id.to_s + set_param(API::Helpers::SUDO_PARAM, user.id.to_s) end it 'raises an error' do - expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError + expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError end end end @@ -396,7 +402,7 @@ describe API::Helpers do token.user = user token.save! - params[API::Helpers::SUDO_PARAM] = user.id.to_s + set_param(API::Helpers::SUDO_PARAM, user.id.to_s) end it 'raises an error' do @@ -420,7 +426,7 @@ describe API::Helpers do context 'passed as param' do before do - params[API::APIGuard::PRIVATE_TOKEN_PARAM] = token.token + set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, token.token) end it_behaves_like 'sudo' @@ -428,7 +434,7 @@ describe API::Helpers do context 'passed as header' do before do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = token.token end it_behaves_like 'sudo' diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index d919899282d..34ecdd1e164 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -203,18 +203,44 @@ describe API::Internal do end context 'with env passed as a JSON' do - it 'sets env in RequestStore' do - expect(Gitlab::Git::Env).to receive(:set).with({ - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' - }) + context 'when relative path envs are not set' do + it 'sets env in RequestStore' do + expect(Gitlab::Git::Env).to receive(:set).with({ + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }) + + push(key, project.wiki, env: { + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' + }.to_json) - push(key, project.wiki, env: { - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' - }.to_json) + expect(response).to have_gitlab_http_status(200) + end + end - expect(response).to have_gitlab_http_status(200) + context 'when relative path envs are set' do + it 'sets env in RequestStore' do + obj_dir_relative = './objects' + alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2'] + repo_path = project.wiki.repository.path_to_repo + + expect(Gitlab::Git::Env).to receive(:set).with({ + 'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative), + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) }, + 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative, + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative + }) + + push(key, project.wiki, env: { + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar', + GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative + }.to_json) + + expect(response).to have_gitlab_http_status(200) + end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a928ba79a4d..91616da6d9a 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -172,15 +172,15 @@ describe API::MergeRequests do context "when authenticated" do it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new do + control = ActiveRecord::QueryRecorder.new do get api("/projects/#{project.id}/merge_requests", user) - end.count + end create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) expect do get api("/projects/#{project.id}/merge_requests", user) - end.not_to exceed_query_limit(control_count) + end.not_to exceed_query_limit(control) end it "returns an array of all merge_requests" do @@ -628,7 +628,7 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:forked_project) { fork_project(project, user2) } + let!(:forked_project) { fork_project(project, user2, repository: true) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 50f6c8b7d64..a41345da05b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -437,6 +437,7 @@ describe API::Projects do project.each_pair do |k, v| next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + expect(json_response[k.to_s]).to eq(v) end @@ -643,6 +644,7 @@ describe API::Projects do expect(response).to have_gitlab_http_status(201) project.each_pair do |k, v| next if %i[has_external_issue_tracker path].include?(k) + expect(json_response[k.to_s]).to eq(v) end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2aeae6f9ec7..2428e63e149 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -510,6 +510,14 @@ describe API::Users do expect(user.reload.notification_email).to eq('new@email.com') end + it 'skips reconfirmation when requested' do + put api("/users/#{user.id}", admin), { skip_reconfirmation: true } + + user.reload + + expect(user.confirmed_at).to be_present + end + it 'updates user with his own username' do put api("/users/#{user.id}", admin), username: user.username diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 91897e5ee01..2e2b9449429 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -314,7 +314,7 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:forked_project) { fork_project(project, user2) } + let!(:forked_project) { fork_project(project, user2, repository: true) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index f62ad747c73..27288b98d1c 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -404,6 +404,7 @@ describe API::V3::Projects do project.each_pair do |k, v| next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + expect(json_response[k.to_s]).to eq(v) end @@ -547,6 +548,7 @@ describe API::V3::Projects do expect(response).to have_gitlab_http_status(201) project.each_pair do |k, v| next if %i[has_external_issue_tracker path].include?(k) + expect(json_response[k.to_s]).to eq(v) end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 0b1f8ce6f6d..1a5ad9b04e4 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -107,6 +107,15 @@ describe 'OpenID Connect requests' do end end + # These 2 calls shouldn't actually throw, they should be handled as an + # unauthorized request, so we should be able to check the response. + # + # This was not possible due to an issue with Warden: + # https://github.com/hassox/warden/pull/162 + # + # When the patch gets merged and we update Warden, these specs will need to + # updated to check the response instead of a raised exception. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/40218 context 'when user is blocked' do it 'returns authentication error' do access_grant @@ -114,7 +123,7 @@ describe 'OpenID Connect requests' do expect do request_access_token - end.to throw_symbol :warden + end.to raise_error UncaughtThrowError end end @@ -125,7 +134,7 @@ describe 'OpenID Connect requests' do expect do request_access_token - end.to throw_symbol :warden + end.to raise_error UncaughtThrowError end end end diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb new file mode 100644 index 00000000000..0fec14d0cce --- /dev/null +++ b/spec/requests/rack_attack_global_spec.rb @@ -0,0 +1,362 @@ +require 'spec_helper' + +describe 'Rack Attack global throttles' do + let(:settings) { Gitlab::CurrentSettings.current_application_settings } + + # Start with really high limits and override them with low limits to ensure + # the right settings are being exercised + let(:settings_to_set) do + { + throttle_unauthenticated_requests_per_period: 100, + throttle_unauthenticated_period_in_seconds: 1, + throttle_authenticated_api_requests_per_period: 100, + throttle_authenticated_api_period_in_seconds: 1, + throttle_authenticated_web_requests_per_period: 100, + throttle_authenticated_web_period_in_seconds: 1 + } + end + + let(:requests_per_period) { 1 } + let(:period_in_seconds) { 10000 } + let(:period) { period_in_seconds.seconds } + + let(:url_that_does_not_require_authentication) { '/users/sign_in' } + let(:url_that_requires_authentication) { '/dashboard/snippets' } + let(:api_partial_url) { '/todos' } + + around do |example| + # Instead of test environment's :null_store so the throttles can increment + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + + # Make time-dependent tests deterministic + Timecop.freeze { example.run } + + Rack::Attack.cache.store = Rails.cache + end + + # Requires let variables: + # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web") + # * get_args + # * other_user_get_args + shared_examples_for 'rate-limited token-authenticated requests' do + before do + # Set low limits + settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period + settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get(*get_args) } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_rejection { get(*get_args) } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_rejection { get(*get_args) } + end + end + + it 'counts requests from different users separately, even from the same IP' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + # would be over the limit if this wasn't a different user + get(*other_user_get_args) + expect(response).to have_http_status 200 + end + + it 'counts all requests from the same user, even via different IPs' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + expect_rejection { get(*get_args) } + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get(*get_args) + expect(response).to have_http_status 200 + end + end + end + end + + describe 'unauthenticated requests' do + before do + # Set low limits + settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period + settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:throttle_unauthenticated_enabled] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get url_that_does_not_require_authentication } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_does_not_require_authentication } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_does_not_require_authentication } + end + end + + it 'counts requests from different IPs separately' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + # would be over limit for the same IP + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:throttle_unauthenticated_enabled] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + end + end + end + + describe 'API requests authenticated with personal access token', :api do + let(:user) { create(:user) } + let(:token) { create(:personal_access_token, user: user) } + let(:other_user) { create(:user) } + let(:other_user_token) { create(:personal_access_token, user: other_user) } + let(:throttle_setting_prefix) { 'throttle_authenticated_api' } + + context 'with the token in the query string' do + let(:get_args) { [api(api_partial_url, personal_access_token: token)] } + let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] } + + it_behaves_like 'rate-limited token-authenticated requests' + end + + context 'with the token in the headers' do + let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) } + let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) } + + it_behaves_like 'rate-limited token-authenticated requests' + end + end + + describe 'API requests authenticated with OAuth token', :api do + let(:user) { create(:user) } + let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } + let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") } + let(:other_user) { create(:user) } + let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) } + let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") } + let(:throttle_setting_prefix) { 'throttle_authenticated_api' } + + context 'with the token in the query string' do + let(:get_args) { [api(api_partial_url, oauth_access_token: token)] } + let(:other_user_get_args) { [api(api_partial_url, oauth_access_token: other_user_token)] } + + it_behaves_like 'rate-limited token-authenticated requests' + end + + context 'with the token in the headers' do + let(:get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) } + let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) } + + it_behaves_like 'rate-limited token-authenticated requests' + end + end + + describe '"web" (non-API) requests authenticated with RSS token' do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + let(:throttle_setting_prefix) { 'throttle_authenticated_web' } + + context 'with the token in the query string' do + let(:get_args) { [rss_url(user), nil] } + let(:other_user_get_args) { [rss_url(other_user), nil] } + + it_behaves_like 'rate-limited token-authenticated requests' + end + end + + describe 'web requests authenticated with regular login' do + let(:user) { create(:user) } + + before do + login_as(user) + + # Set low limits + settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period + settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:throttle_authenticated_web_enabled] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get url_that_requires_authentication } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_requires_authentication } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_requires_authentication } + end + end + + it 'counts requests from different users separately, even from the same IP' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + # would be over the limit if this wasn't a different user + login_as(create(:user)) + + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + it 'counts all requests from the same user, even via different IPs' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + expect_rejection { get url_that_requires_authentication } + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:throttle_authenticated_web_enabled] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + end + end + end + + def api_get_args_with_token_headers(partial_url, token_headers) + ["/api/#{API::API.version}#{partial_url}", nil, token_headers] + end + + def rss_url(user) + "/dashboard/projects.atom?rss_token=#{user.rss_token}" + end + + def private_token_headers(user) + { 'HTTP_PRIVATE_TOKEN' => user.private_token } + end + + def personal_access_token_headers(personal_access_token) + { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token } + end + + def oauth_token_headers(oauth_access_token) + { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" } + end + + def expect_rejection(&block) + yield + + expect(response).to have_http_status(429) + end +end diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb index 7a4c8304e62..71788028cbf 100644 --- a/spec/routing/group_routing_spec.rb +++ b/spec/routing/group_routing_spec.rb @@ -39,13 +39,19 @@ describe "Groups", "routing" do describe 'legacy redirection' do describe 'labels' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels", "/groups/complex.group-namegit/-/labels/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels", "/groups/complex.group-namegit/-/labels" do let(:resource) { create(:group, parent: group, path: 'labels') } end + + context 'when requesting JSON' do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels.json", "/groups/complex.group-namegit/-/labels.json" do + let(:resource) { create(:group, parent: group, path: 'labels') } + end + end end describe 'group_members' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/group_members", "/groups/complex.group-namegit/-/group_members/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/group_members", "/groups/complex.group-namegit/-/group_members" do let(:resource) { create(:group, parent: group, path: 'group_members') } end end @@ -60,7 +66,7 @@ describe "Groups", "routing" do end describe 'milestones' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones", "/groups/complex.group-namegit/-/milestones/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones", "/groups/complex.group-namegit/-/milestones" do let(:resource) { create(:group, parent: group, path: 'milestones') } end @@ -76,18 +82,18 @@ describe "Groups", "routing" do end context 'with a query string' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?hello=world", "/groups/complex.group-namegit/-/milestones/?hello=world" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?hello=world", "/groups/complex.group-namegit/-/milestones?hello=world" do let(:resource) { create(:group, parent: group, path: 'milestones') } end - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?milestones=/milestones", "/groups/complex.group-namegit/-/milestones/?milestones=/milestones" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?milestones=/milestones", "/groups/complex.group-namegit/-/milestones?milestones=/milestones" do let(:resource) { create(:group, parent: group, path: 'milestones') } end end end describe 'edit' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/edit", "/groups/complex.group-namegit/-/edit/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/edit", "/groups/complex.group-namegit/-/edit" do let(:resource) do pending('still rejected because of the wildcard reserved word') create(:group, parent: group, path: 'edit') @@ -96,29 +102,29 @@ describe "Groups", "routing" do end describe 'issues' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/issues", "/groups/complex.group-namegit/-/issues/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/issues", "/groups/complex.group-namegit/-/issues" do let(:resource) { create(:group, parent: group, path: 'issues') } end end describe 'merge_requests' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/merge_requests", "/groups/complex.group-namegit/-/merge_requests/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/merge_requests", "/groups/complex.group-namegit/-/merge_requests" do let(:resource) { create(:group, parent: group, path: 'merge_requests') } end end describe 'projects' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/projects", "/groups/complex.group-namegit/-/projects/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/projects", "/groups/complex.group-namegit/-/projects" do let(:resource) { create(:group, parent: group, path: 'projects') } end end describe 'activity' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/activity", "/groups/complex.group-namegit/-/activity/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/activity", "/groups/complex.group-namegit/-/activity" do let(:resource) { create(:group, parent: group, path: 'activity') } end - it_behaves_like 'redirecting a legacy path', "/groups/activity/activity", "/groups/activity/-/activity/" do + it_behaves_like 'redirecting a legacy path', "/groups/activity/activity", "/groups/activity/-/activity" do let!(:parent) { create(:group, path: 'activity') } let(:resource) { create(:group, parent: parent, path: 'activity') } end diff --git a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb new file mode 100644 index 00000000000..8899dc85384 --- /dev/null +++ b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/line_break_after_guard_clauses' + +describe RuboCop::Cop::LineBreakAfterGuardClauses do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples 'examples with guard clause' do |title| + %w[if unless].each do |conditional| + it "flags violation for #{title} #{conditional} without line breaks" do + source = <<~RUBY + #{title} #{conditional} condition + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(1) + expect(cop.highlights).to eq(["#{title} #{conditional} condition"]) + expect(offense.message).to eq('Add a line break after guard clauses') + end + + it "doesn't flag violation for #{title} #{conditional} with line break" do + source = <<~RUBY + #{title} #{conditional} condition + + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} on multiple lines without line break" do + source = <<~RUBY + #{conditional} condition + #{title} + end + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by end keyword" do + source = <<~RUBY + def test + #{title} #{conditional} condition + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by elsif keyword" do + source = <<~RUBY + if model + #{title} #{conditional} condition + elsif + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by else keyword" do + source = <<~RUBY + if model + #{title} #{conditional} condition + else + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by when keyword" do + source = <<~RUBY + case model + when condition_a + #{title} #{conditional} condition + when condition_b + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by rescue keyword" do + source = <<~RUBY + begin + #{title} #{conditional} condition + rescue StandardError + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by ensure keyword" do + source = <<~RUBY + def foo + #{title} #{conditional} condition + ensure + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by another guard clause" do + source = <<~RUBY + #{title} #{conditional} condition + #{title} #{conditional} condition + + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "autocorrects #{title} #{conditional} guard clauses without line break" do + source = <<~RUBY + #{title} #{conditional} condition + do_stuff + RUBY + autocorrected = autocorrect_source(cop, source) + + expected_source = <<~RUBY + #{title} #{conditional} condition + + do_stuff + RUBY + expect(autocorrected).to eql(expected_source) + end + end + end + + %w[return fail raise next break throw].each do |example| + it_behaves_like 'examples with guard clause', example + end +end diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb deleted file mode 100644 index 07cb3fc4a2e..00000000000 --- a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table' - -describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable 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::LARGE_TABLES.each do |table| - it "registers an offense for the #{table} table" do - inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - end - end - end - - it 'registers no offense for non-blacklisted tables' do - inspect_source(cop, "add_column_with_default :table, :column, default: true") - - expect(cop.offenses).to be_empty - end - end - - context 'outside of migration' do - it 'registers no offense' do - table = described_class::LARGE_TABLES.sample - inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") - - expect(cop.offenses).to be_empty - end - end -end diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb new file mode 100644 index 00000000000..17b19e139e4 --- /dev/null +++ b/spec/rubocop/cop/migration/update_large_table_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/update_large_table' + +describe RuboCop::Cop::Migration::UpdateLargeTable do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + shared_examples 'large tables' do |update_method| + described_class::LARGE_TABLES.each do |table| + it "registers an offense for the #{table} table" do + inspect_source(cop, "#{update_method} :#{table}, :column, default: true") + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + end + + context 'for the add_column_with_default method' do + include_examples 'large tables', 'add_column_with_default' + end + + context 'for the update_column_in_batches method' do + include_examples 'large tables', 'update_column_in_batches' + end + + it 'registers no offense for non-blacklisted tables' do + inspect_source(cop, "add_column_with_default :table, :column, default: true") + + expect(cop.offenses).to be_empty + end + + it 'registers no offense for non-blacklisted methods' do + table = described_class::LARGE_TABLES.sample + + inspect_source(cop, "some_other_method :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + end + + context 'outside of migration' do + let(:table) { described_class::LARGE_TABLES.sample } + + it 'registers no offense for add_column_with_default' do + inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + + it 'registers no offense for update_column_in_batches' do + inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb new file mode 100644 index 00000000000..5ec8ed0976d --- /dev/null +++ b/spec/services/base_count_service_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe BaseCountService, :use_clean_rails_memory_store_caching do + let(:service) { described_class.new } + + describe '#relation_for_count' do + it 'raises NotImplementedError' do + expect { service.relation_for_count }.to raise_error(NotImplementedError) + end + end + + describe '#count' do + it 'returns the number of values' do + expect(service) + .to receive(:cache_key) + .and_return('foo') + + expect(service) + .to receive(:uncached_count) + .and_return(5) + + expect(service.count).to eq(5) + end + end + + describe '#uncached_count' do + it 'returns the uncached number of values' do + expect(service) + .to receive(:relation_for_count) + .and_return(double(:relation, count: 5)) + + expect(service.uncached_count).to eq(5) + end + end + + describe '#refresh_cache' do + it 'refreshes the cache' do + allow(service) + .to receive(:cache_key) + .and_return('foo') + + allow(service) + .to receive(:uncached_count) + .and_return(4) + + service.refresh_cache + + expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to eq(4) + end + end + + describe '#delete_cache' do + it 'deletes the cache' do + allow(service) + .to receive(:cache_key) + .and_return('foo') + + allow(service) + .to receive(:uncached_count) + .and_return(4) + + service.refresh_cache + service.delete_cache + + expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to be_nil + end + end + + describe '#raw?' do + it 'returns false' do + expect(service.raw?).to eq(false) + end + end + + describe '#cache_key' do + it 'raises NotImplementedError' do + expect { service.cache_key }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb index 16e288b3148..af35e17bfa7 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -5,7 +5,7 @@ describe Milestones::DestroyService do let(:project) { create(:project) } let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } let!(:issue) { create(:issue, project: project, milestone: milestone) } - let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } before do project.team << [user, :master] diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb index 9f2df6d6d19..a0a2843b676 100644 --- a/spec/services/milestones/promote_service_spec.rb +++ b/spec/services/milestones/promote_service_spec.rb @@ -25,6 +25,18 @@ describe Milestones::PromoteService do expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError) end + + it 'does not promote milestone and update issuables if promoted milestone is not valid' do + issue = create(:issue, milestone: milestone, project: project) + merge_request = create(:merge_request, milestone: milestone, source_project: project) + allow_any_instance_of(Milestone).to receive(:valid?).and_return(false) + + expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError) + + expect(milestone.reload).to be_persisted + expect(issue.reload.milestone).to eq(milestone) + expect(merge_request.reload.milestone).to eq(milestone) + end end context 'without duplicated milestone titles across projects' do @@ -34,6 +46,16 @@ describe Milestones::PromoteService do expect(promoted_milestone).to be_group_milestone end + it 'does not update issuables without milestone with the new promoted milestone' do + issue_without_milestone = create(:issue, project: project, milestone: nil) + merge_request_without_milestone = create(:merge_request, milestone: nil, source_project: project) + + service.execute(milestone) + + expect(issue_without_milestone.reload.milestone).to be_nil + expect(merge_request_without_milestone.reload.milestone).to be_nil + end + it 'sets issuables with new promoted milestone' do issue = create(:issue, milestone: milestone, project: project) merge_request = create(:merge_request, milestone: milestone, source_project: project) @@ -59,6 +81,20 @@ describe Milestones::PromoteService do expect(Milestone.exists?(milestone_2.id)).to be_falsy end + it 'does not update issuables without milestone with the new promoted milestone' do + issue_without_milestone_1 = create(:issue, project: project, milestone: nil) + issue_without_milestone_2 = create(:issue, project: project_2, milestone: nil) + merge_request_without_milestone_1 = create(:merge_request, milestone: nil, source_project: project) + merge_request_without_milestone_2 = create(:merge_request, milestone: nil, source_project: project_2) + + service.execute(milestone) + + expect(issue_without_milestone_1.reload.milestone).to be_nil + expect(issue_without_milestone_2.reload.milestone).to be_nil + expect(merge_request_without_milestone_1.reload.milestone).to be_nil + expect(merge_request_without_milestone_2.reload.milestone).to be_nil + end + it 'sets all issuables with new promoted milestone' do issue = create(:issue, milestone: milestone, project: project) issue_2 = create(:issue, milestone: milestone_2, project: project_2) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index b13e12e7c94..db5de572b6d 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -280,6 +280,7 @@ describe NotificationService, :mailer do next if member.id == @u_disabled.id # Author should not be notified next if member.id == note.author.id + should_email(member) end @@ -327,6 +328,7 @@ describe NotificationService, :mailer do next if member.id == @u_disabled.id # Author should not be notified next if member.id == note.author.id + should_email(member) end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 2459f371a91..2b1337bee7e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -42,6 +42,18 @@ describe Projects::TransferService do expect(service).to receive(:execute_system_hooks) end end + + it 'disk path has moved' do + old_path = project.repository.disk_path + old_full_path = project.repository.full_path + + transfer_project(project, user, group) + + expect(project.repository.disk_path).not_to eq(old_path) + expect(project.repository.full_path).not_to eq(old_full_path) + expect(project.disk_path).not_to eq(old_path) + expect(project.disk_path).to start_with(group.path) + end end context 'when transfer fails' do @@ -188,6 +200,26 @@ describe Projects::TransferService do end end + context 'when hashed storage in use' do + let(:hashed_project) { create(:project, :repository, :hashed, namespace: user.namespace) } + + before do + group.add_owner(user) + end + + it 'does not move the directory' do + old_path = hashed_project.repository.disk_path + old_full_path = hashed_project.repository.full_path + + transfer_project(hashed_project, user, group) + project.reload + + expect(hashed_project.repository.disk_path).to eq(old_path) + expect(hashed_project.repository.full_path).to eq(old_full_path) + expect(hashed_project.disk_path).to eq(old_path) + end + end + describe 'refreshing project authorizations' do let(:group) { create(:group) } let(:owner) { project.namespace.owner } diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb new file mode 100644 index 00000000000..a188cf86772 --- /dev/null +++ b/spec/services/users/keys_count_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Users::KeysCountService, :use_clean_rails_memory_store_caching do + let(:user) { create(:user) } + let(:service) { described_class.new(user) } + + describe '#count' do + before do + create(:personal_key, user: user) + end + + it 'returns the number of SSH keys as an Integer' do + expect(service.count).to eq(1) + end + + it 'caches the number of keys in Redis' do + service.delete_cache + + recorder = ActiveRecord::QueryRecorder.new do + 2.times { service.count } + end + + expect(recorder.count).to eq(1) + end + end + + describe '#refresh_cache' do + it 'refreshes the Redis cache' do + Rails.cache.write(service.cache_key, 10) + service.refresh_cache + + expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_zero + end + end + + describe '#delete_cache' do + it 'removes the cache' do + service.count + service.delete_cache + + expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_nil + end + end + + describe '#uncached_count' do + it 'returns the number of SSH keys' do + expect(service.uncached_count).to be_zero + end + + it 'does not cache the number of keys' do + recorder = ActiveRecord::QueryRecorder.new do + 2.times { service.uncached_count } + end + + expect(recorder.count).to be > 0 + end + end + + describe '#cache_key' do + it 'returns the cache key' do + expect(service.cache_key).to eq("users/key-count-service/#{user.id}") + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7c8331f6c60..6310ea1b52b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,6 +50,7 @@ RSpec.configure do |config| config.include SearchHelpers, type: :feature config.include CookieHelper, :js config.include InputHelper, :js + config.include SelectionHelper, :js config.include InspectRequests, :js config.include WaitForRequests, :js config.include LiveDebugger, :js diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb index 5515c355cea..128aaaf25fe 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/fixture_helpers.rb @@ -1,6 +1,7 @@ module FixtureHelpers def fixture_file(filename) return '' if filename.blank? + File.read(expand_fixture_path(filename)) end diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index ef3c8e7087f..4ee33f9725b 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -33,6 +33,7 @@ end def capture!(cmd, dir) output = IO.popen(cmd, 'r', chdir: dir) { |io| io.read } raise "command failed with #{$?}: #{cmd.join(' ')}" unless $?.success? + output.chomp end diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb index 1512b3e0620..c7e8a39a617 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -4,6 +4,7 @@ RSpec.configure do |config| allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) else next if example.metadata[:skip_gitaly_mock] + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) end end diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb index ba0b805caad..369775db462 100644 --- a/spec/support/query_recorder.rb +++ b/spec/support/query_recorder.rb @@ -8,7 +8,14 @@ module ActiveRecord ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) end + def show_backtrace(values) + Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}") + caller.each { |line| Rails.logger.debug(" --> #{line}") } + end + def callback(name, start, finish, message_id, values) + show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG'] + if values[:name]&.include?("CACHE") @cached << values[:sql] elsif !values[:name]&.include?("SCHEMA") @@ -69,10 +76,17 @@ RSpec::Matchers.define :exceed_query_limit do |expected| @recorder.count end + def count_queries(queries) + queries.each_with_object(Hash.new(0)) { |query, counts| counts[query] += 1 } + end + def log_message if expected.is_a?(ActiveRecord::QueryRecorder) - extra_queries = (expected.log - @recorder.log).join("\n\n") - "Extra queries: \n\n #{extra_queries}" + counts = count_queries(expected.log) + extra_queries = @recorder.log.reject { |query| counts[query] -= 1 unless counts[query].zero? } + extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" } + + (['Extra queries:'] + extra_queries_display).join("\n\n") else @recorder.log_message end diff --git a/spec/support/selection_helper.rb b/spec/support/selection_helper.rb new file mode 100644 index 00000000000..b4725b137b2 --- /dev/null +++ b/spec/support/selection_helper.rb @@ -0,0 +1,6 @@ +module SelectionHelper + def select_element(selector) + find(selector) + execute_script("let range = document.createRange(); let sel = window.getSelection(); range.selectNodeContents(document.querySelector('#{selector}')); sel.addRange(range);") + end +end diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb new file mode 100644 index 00000000000..641eccfd334 --- /dev/null +++ b/spec/tasks/gitlab/cleanup_rake_spec.rb @@ -0,0 +1,41 @@ +require 'rake_helper' + +describe 'gitlab:cleanup rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/cleanup' + end + + context 'cleanup repositories' do + let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address } + let(:storages) do + { + 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address } + } + end + + before do + FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage')) + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + end + + after do + FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) + end + + it 'moves it to an orphaned path' do + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git')) + run_rake_task('gitlab:cleanup:repos') + repo_list = Dir['tmp/tests/default_storage/broken/*'] + + expect(repo_list.first).to include('+orphaned+') + end + + it 'ignores @hashed repos' do + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) + + run_rake_task('gitlab:cleanup:repos') + + expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy + end + end +end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 5dd8fe8eaa5..6aba86fdc3c 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -47,7 +47,7 @@ describe 'gitlab:gitaly namespace rake task' do stub_env('CI', false) 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') + allow(Rails.env).to receive(:test?).and_return(false) end context 'gmake is available' do @@ -57,7 +57,7 @@ describe 'gitlab:gitaly namespace rake task' do 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(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -70,18 +70,20 @@ describe 'gitlab:gitaly namespace rake task' do end it 'calls make in the gitaly directory' do - expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end - context 'when Rails.env is not "test"' do + context 'when Rails.env is test' do + let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] } + before do - allow(Rails.env).to receive(:test?).and_return(false) + allow(Rails.env).to receive(:test?).and_return(true) 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) + it 'calls make in the gitaly directory with --no-deployment flag for bundle' do + expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -110,6 +112,7 @@ describe 'gitlab:gitaly namespace rake task' do expected_output = <<~TOML # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} # This is in TOML format suitable for use in Gitaly's config.toml file. + bin_dir = "tmp/tests/gitaly" socket_path = "/path/to/my.socket" [gitlab-shell] dir = "#{Gitlab.config.gitlab_shell.path}" diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb index 41de94d35c2..79a566975df 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/unicorn/unicorn_spec.rb @@ -71,6 +71,7 @@ describe 'Unicorn' do timeout = 5 * 60 timeout.times do return if File.exist?(ready_file) + pid = Process.waitpid(master_pid, Process::WNOHANG) raise "unicorn failed to boot: #{$?}" unless pid.nil? diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index f52b2bab05b..fd195d6f9b8 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -28,25 +28,51 @@ describe FileUploader do end context 'hashed storage' do - let(:project) { build_stubbed(:project, :hashed) } + context 'when rolled out attachments' do + let(:project) { build_stubbed(:project, :hashed) } - describe '.absolute_path' do - it 'returns the correct absolute path by building it dynamically' do - upload = double(model: project, path: 'secret/foo.jpg') + describe '.absolute_path' do + it 'returns the correct absolute path by building it dynamically' do + upload = double(model: project, path: 'secret/foo.jpg') - dynamic_segment = project.disk_path + dynamic_segment = project.disk_path - expect(described_class.absolute_path(upload)) - .to end_with("#{dynamic_segment}/secret/foo.jpg") + expect(described_class.absolute_path(upload)) + .to end_with("#{dynamic_segment}/secret/foo.jpg") + end + end + + describe "#store_dir" do + it "stores in the namespace path" do + uploader = described_class.new(project) + + expect(uploader.store_dir).to include(project.disk_path) + expect(uploader.store_dir).not_to include("system") + end end end - describe "#store_dir" do - it "stores in the namespace path" do - uploader = described_class.new(project) + context 'when only repositories are rolled out' do + let(:project) { build_stubbed(:project, storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) } - expect(uploader.store_dir).to include(project.disk_path) - expect(uploader.store_dir).not_to include("system") + describe '.absolute_path' do + it 'returns the correct absolute path by building it dynamically' do + upload = double(model: project, path: 'secret/foo.jpg') + + dynamic_segment = project.full_path + + expect(described_class.absolute_path(upload)) + .to end_with("#{dynamic_segment}/secret/foo.jpg") + end + end + + describe "#store_dir" do + it "stores in the namespace path" do + uploader = described_class.new(project) + + expect(uploader.store_dir).to include(project.full_path) + expect(uploader.store_dir).not_to include("system") + end end end end diff --git a/yarn.lock b/yarn.lock index 1271c8a1ee3..9bdf5e0f64b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -264,6 +264,12 @@ aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios-mock-adapter@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d" + dependencies: + deep-equal "^1.0.1" + axios@^0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" @@ -922,6 +928,17 @@ binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" +blackst0ne-mermaid@^7.1.0-fixed: + version "7.1.0-fixed" + resolved "https://registry.yarnpkg.com/blackst0ne-mermaid/-/blackst0ne-mermaid-7.1.0-fixed.tgz#3707b3a113d78610e3068e18a588f46b4688de49" + dependencies: + d3 "3.5.17" + dagre-d3-renderer "^0.4.24" + dagre-layout "^0.8.0" + he "^1.1.1" + lodash "^4.17.4" + moment "^2.18.1" + blob@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" @@ -1638,6 +1655,10 @@ custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" +d3@3.5.17: + version "3.5.17" + resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" + d3@^3.5.11: version "3.5.11" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c" @@ -1654,6 +1675,22 @@ d@^0.1.1: dependencies: es5-ext "~0.10.2" +dagre-d3-renderer@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.4.24.tgz#b36ce2fe4ea20de43e7698627c6ede2a9f15ec45" + dependencies: + d3 "3.5.17" + dagre-layout "^0.8.0" + graphlib "^2.1.1" + lodash "^4.17.4" + +dagre-layout@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/dagre-layout/-/dagre-layout-0.8.0.tgz#7147b6afb655602f855158dfea171db9aa98d4ff" + dependencies: + graphlib "^2.1.1" + lodash "^4.17.4" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2852,6 +2889,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +graphlib@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.1.tgz#42352c52ba2f4d035cb566eb91f7395f76ebc951" + dependencies: + lodash "^4.11.1" + gzip-size@3.0.0, gzip-size@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520" @@ -2946,7 +2989,7 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" -he@^1.1.0: +he@^1.1.0, he@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -3942,7 +3985,7 @@ lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4156,6 +4199,10 @@ moment@2.x: version "2.17.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" +moment@^2.18.1: + version "2.19.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" + monaco-editor@0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.0.tgz#6604932585fe9c1f993f000a503d0d20fbe5896a" |