diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 059b181bb1c..5b39304444c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,6 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.29" variables: - MYSQL_ALLOW_EMPTY_PASSWORD: "1" RAILS_ENV: "test" NODE_ENV: "test" SIMPLECOV: "true" @@ -37,6 +36,7 @@ include: - local: .gitlab/ci/cng.gitlab-ci.yml - local: .gitlab/ci/docs.gitlab-ci.yml - local: .gitlab/ci/frontend.gitlab-ci.yml + - local: .gitlab/ci/memory.gitlab-ci.yml - local: .gitlab/ci/pages.gitlab-ci.yml - local: .gitlab/ci/qa.gitlab-ci.yml - local: .gitlab/ci/reports.gitlab-ci.yml diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml new file mode 100644 index 00000000000..50b843df585 --- /dev/null +++ b/.gitlab/ci/memory.gitlab-ci.yml @@ -0,0 +1,19 @@ +memory-static: + extends: .dedicated-no-docs-no-db-pull-cache-job + script: + # Uses two different reports from the 'derailed_benchmars' gem. + + # Loads each of gems in the Gemfile and checks how much memory they consume when they are required. + # 'derailed_benchmarks' internally uses 'get_process_mem' + - scripts/memory-static 'tmp/memory_static_full_report.txt' 'tmp/memory_static_metrics.txt' + + # Outputs detailed information about objects created while gems are loaded. + # 'derailed_benchmarks' internally uses 'memory_profiler' + - scripts/memory-static-objects 'tmp/memory_static_objects_full_report.txt' 'tmp/memory_static_metrics.txt' + artifacts: + paths: + - tmp/memory_static_full_report.txt + - tmp/memory_static_objects_full_report.txt + - tmp/memory_static_metrics.txt + reports: + metrics: tmp/memory_static_metrics.txt diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 8534b15e16b..529a0de696b 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -10,11 +10,6 @@ command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:alpine -.use-mysql: &use-mysql - services: - - mysql:5.7 - - redis:alpine - .only-schedules-master: &only-schedules-master only: - schedules@gitlab-org/gitlab-ce @@ -94,10 +89,6 @@ <<: *use-pg-10 image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.29" -.rspec-metadata-mysql: &rspec-metadata-mysql - <<: *rspec-metadata - <<: *use-mysql - # DB migration, rollback, and seed jobs .db-migrate-reset: &db-migrate-reset extends: .dedicated-no-docs-and-no-qa-pull-cache-job @@ -173,42 +164,6 @@ rspec system pg-10: <<: *only-schedules-master parallel: 24 -rspec unit mysql: - <<: *rspec-metadata-mysql - <<: *only-schedules-master - parallel: 20 - -rspec integration mysql: - <<: *rspec-metadata-mysql - <<: *only-schedules-master - parallel: 6 - -rspec system mysql: - <<: *rspec-metadata-mysql - <<: *only-schedules-master - parallel: 24 - -.rspec-mysql-on-demand: &rspec-mysql-on-demand - only: - variables: - - $CI_COMMIT_MESSAGE =~ /\[run mysql\]/i - - $CI_COMMIT_REF_NAME =~ /mysql/ - -rspec unit mysql on-demand: - <<: *rspec-metadata-mysql - <<: *rspec-mysql-on-demand - parallel: 20 - -rspec integration mysql on-demand: - <<: *rspec-metadata-mysql - <<: *rspec-mysql-on-demand - parallel: 6 - -rspec system mysql on-demand: - <<: *rspec-metadata-mysql - <<: *rspec-mysql-on-demand - parallel: 24 - rspec-fast-spec-helper: <<: *rspec-metadata-pg script: @@ -226,12 +181,6 @@ rspec quarantine pg: <<: *rspec-quarantine allow_failure: true -rspec quarantine mysql: - <<: *rspec-metadata-mysql - <<: *rspec-quarantine - <<: *only-schedules-master - allow_failure: true - static-analysis: extends: .dedicated-no-docs-no-db-pull-cache-job dependencies: @@ -281,10 +230,6 @@ db:migrate:reset-pg: <<: *db-migrate-reset <<: *use-pg -db:migrate:reset-mysql: - <<: *db-migrate-reset - <<: *use-mysql - db:check-schema-pg: <<: *db-migrate-reset <<: *use-pg @@ -295,10 +240,6 @@ migration:path-pg: <<: *migration-paths <<: *use-pg -migration:path-mysql: - <<: *migration-paths - <<: *use-mysql - .db-rollback: &db-rollback extends: .dedicated-no-docs-and-no-qa-pull-cache-job script: @@ -311,22 +252,12 @@ db:rollback-pg: <<: *db-rollback <<: *use-pg -db:rollback-mysql: - <<: *db-rollback - <<: *use-mysql - gitlab:setup-pg: <<: *gitlab-setup <<: *use-pg dependencies: - setup-test-env -gitlab:setup-mysql: - <<: *gitlab-setup - <<: *use-mysql - dependencies: - - setup-test-env - coverage: # Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to # download artifacts from all the rspec jobs instead of from setup-test-env only diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 7cebb88f3a4..4f66a5d080c 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -12,6 +12,7 @@ const Api = { groupProjectsPath: '/api/:version/groups/:id/projects.json', projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', + forkedProjectsPath: '/api/:version/projects/:id/forks', projectLabelsPath: '/:namespace_path/:project_path/-/labels', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', @@ -113,6 +114,21 @@ const Api = { return axios.get(url); }, + /** + * Get all projects for a forked relationship to a specified project + * @param {string} projectPath - Path or ID of a project + * @param {Object} params - Get request parameters + * @returns {Promise} - Request promise + */ + projectForks(projectPath, params) { + const url = Api.buildUrl(Api.forkedProjectsPath).replace( + ':id', + encodeURIComponent(projectPath), + ); + + return axios.get(url, { params }); + }, + /** * Get all Merge Requests for a project, eventually filtering based on * supplied parameters diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue index 575c860851c..d435460e38f 100644 --- a/app/assets/javascripts/issuable_suggestions/components/app.vue +++ b/app/assets/javascripts/issuable_suggestions/components/app.vue @@ -4,7 +4,7 @@ import { GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import Suggestion from './item.vue'; -import query from '../queries/issues.graphql'; +import query from '../queries/issues.query.graphql'; export default { components: { diff --git a/app/assets/javascripts/issuable_suggestions/queries/issues.graphql b/app/assets/javascripts/issuable_suggestions/queries/issues.query.graphql similarity index 100% rename from app/assets/javascripts/issuable_suggestions/queries/issues.graphql rename to app/assets/javascripts/issuable_suggestions/queries/issues.query.graphql diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue index 8e2d8fa816a..c203cb0667c 100644 --- a/app/assets/javascripts/mr_popover/components/mr_popover.vue +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -3,7 +3,7 @@ import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; import Icon from '../../vue_shared/components/icon.vue'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; import timeagoMixin from '../../vue_shared/mixins/timeago'; -import query from '../queries/merge_request.graphql'; +import query from '../queries/merge_request.query.graphql'; import { mrStates, humanMRStates } from '../constants'; export default { diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.graphql b/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql similarity index 100% rename from app/assets/javascripts/mr_popover/queries/merge_request.graphql rename to app/assets/javascripts/mr_popover/queries/merge_request.query.graphql diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index 3f5a3e15c2c..55bc93a2b13 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -7,8 +7,8 @@ import OAuthRememberMe from './oauth_remember_me'; import preserveUrlFragment from './preserve_url_fragment'; document.addEventListener('DOMContentLoaded', () => { - new LengthValidator(); // eslint-disable-line no-new new UsernameValidator(); // eslint-disable-line no-new + new LengthValidator(); // eslint-disable-line no-new new SigninTabsMemoizer(); // eslint-disable-line no-new new NoEmojiValidator(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 7a41805bada..36d1e773134 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -1,133 +1,79 @@ -/* eslint-disable consistent-return, class-methods-use-this */ +import InputValidator from '~/validators/input_validator'; -import $ from 'jquery'; import _ from 'underscore'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; const debounceTimeoutDuration = 1000; +const rootUrl = gon.relative_url_root; const invalidInputClass = 'gl-field-error-outline'; const successInputClass = 'gl-field-success-outline'; -const unavailableMessageSelector = '.username .validation-error'; -const successMessageSelector = '.username .validation-success'; -const pendingMessageSelector = '.username .validation-pending'; -const invalidMessageSelector = '.username .gl-field-error'; +const successMessageSelector = '.validation-success'; +const pendingMessageSelector = '.validation-pending'; +const unavailableMessageSelector = '.validation-error'; -export default class UsernameValidator { - constructor() { - this.inputElement = $('#new_user_username'); - this.inputDomElement = this.inputElement.get(0); - this.state = { - available: false, - valid: false, - pending: false, - empty: true, - }; +export default class UsernameValidator extends InputValidator { + constructor(opts = {}) { + super(); - const debounceTimeout = _.debounce(username => { - this.validateUsername(username); + const container = opts.container || ''; + const validateLengthElements = document.querySelectorAll(`${container} .js-validate-username`); + + this.debounceValidateInput = _.debounce(inputDomElement => { + UsernameValidator.validateUsernameInput(inputDomElement); }, debounceTimeoutDuration); - this.inputElement.on('keyup.username_check', () => { - const username = this.inputElement.val(); - - this.state.valid = this.inputDomElement.validity.valid; - this.state.empty = !username.length; - - if (this.state.valid) { - return debounceTimeout(username); - } - - this.renderState(); - }); - - // Override generic field validation - this.inputElement.on('invalid', this.interceptInvalid.bind(this)); + validateLengthElements.forEach(element => + element.addEventListener('input', this.eventHandler.bind(this)), + ); } - renderState() { - // Clear all state - this.clearFieldValidationState(); + eventHandler(event) { + const inputDomElement = event.target; - if (this.state.valid && this.state.available) { - return this.setSuccessState(); - } - - if (this.state.empty) { - return this.clearFieldValidationState(); - } - - if (this.state.pending) { - return this.setPendingState(); - } - - if (!this.state.valid) { - return this.setInvalidState(); - } - - if (!this.state.available) { - return this.setUnavailableState(); - } + UsernameValidator.resetInputState(inputDomElement); + this.debounceValidateInput(inputDomElement); } - interceptInvalid(event) { - event.preventDefault(); - event.stopPropagation(); - } + static validateUsernameInput(inputDomElement) { + const username = inputDomElement.value; - validateUsername(username) { - if (this.state.valid) { - this.state.pending = true; - this.state.available = false; - this.renderState(); - axios - .get(`${gon.relative_url_root}/users/${username}/exists`) - .then(({ data }) => this.setAvailabilityState(data.exists)) + if (inputDomElement.checkValidity() && username.length > 0) { + UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); + UsernameValidator.fetchUsernameAvailability(username) + .then(usernameTaken => { + UsernameValidator.setInputState(inputDomElement, !usernameTaken); + UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector, false); + UsernameValidator.setMessageVisibility( + inputDomElement, + usernameTaken ? unavailableMessageSelector : successMessageSelector, + ); + }) .catch(() => flash(__('An error occurred while validating username'))); } } - setAvailabilityState(usernameTaken) { - if (usernameTaken) { - this.state.available = false; - } else { - this.state.available = true; + static fetchUsernameAvailability(username) { + return axios.get(`${rootUrl}/users/${username}/exists`).then(({ data }) => data.exists); + } + + static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) { + const messageElement = inputDomElement.parentElement.querySelector(messageSelector); + messageElement.classList.toggle('hide', !isVisible); + } + + static setInputState(inputDomElement, success = true) { + inputDomElement.classList.toggle(successInputClass, success); + inputDomElement.classList.toggle(invalidInputClass, !success); + } + + static resetInputState(inputDomElement) { + UsernameValidator.setMessageVisibility(inputDomElement, successMessageSelector, false); + UsernameValidator.setMessageVisibility(inputDomElement, unavailableMessageSelector, false); + + if (inputDomElement.checkValidity()) { + inputDomElement.classList.remove(successInputClass, invalidInputClass); } - this.state.pending = false; - this.renderState(); - } - - clearFieldValidationState() { - this.inputElement.siblings('p').hide(); - - this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass); - } - - setUnavailableState() { - const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); - this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); - $usernameUnavailableMessage.show(); - } - - setSuccessState() { - const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); - this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); - $usernameSuccessMessage.show(); - } - - setPendingState() { - const $usernamePendingMessage = $(pendingMessageSelector); - if (this.state.pending) { - $usernamePendingMessage.show(); - } else { - $usernamePendingMessage.hide(); - } - } - - setInvalidState() { - const $inputErrorMessage = $(invalidMessageSelector); - this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); - $inputErrorMessage.show(); } } diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 6eca015036f..0d4d431855c 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -1,6 +1,6 @@