diff options
225 files changed, 3621 insertions, 1221 deletions
diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md index 246f2dae009..42314f9b2dd 100644 --- a/.gitlab/merge_request_templates/Security Release.md +++ b/.gitlab/merge_request_templates/Security Release.md @@ -7,6 +7,10 @@ See [the general developer security release guidelines](https://gitlab.com/gitla This merge request _must not_ close the corresponding security issue _unless_ it targets master. +When submitting a merge request for CE, a corresponding EE merge request is +always required. This makes it easier to merge security merge requests, as +manually merging CE into EE is no longer required. + --> ## Related issues @@ -20,8 +24,8 @@ targets master. - [ ] Title of this MR is the same as for all backports - [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security` - [ ] Add a link to this MR in the `links` section of related issue -- [ ] Add a link to an EE MR if required -- [ ] Assign to a reviewer +- [ ] Set up an EE MR (always required for CE merge requests): EE_MR_LINK_HERE +- [ ] Assign to a reviewer (that is not a release manager) ## Reviewer checklist @@ -116,7 +116,7 @@ gem 'seed-fu', '~> 2.3.7' # Markdown and HTML processing gem 'html-pipeline', '~> 2.8' gem 'deckar01-task_list', '2.2.0' -gem 'gitlab-markup', '~> 1.6.5' +gem 'gitlab-markup', '~> 1.7.0' gem 'github-markup', '~> 1.7.0', require: 'github/markup' gem 'commonmarker', '~> 0.17' gem 'RedCloth', '~> 4.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 4c88afc7e24..4ebcc6c81b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -262,7 +262,7 @@ GEM foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) - fugit (1.1.7) + fugit (1.1.9) et-orbi (~> 1.1, >= 1.1.7) raabro (~> 1.1) fuubar (2.2.0) @@ -286,7 +286,7 @@ GEM github-markup (1.7.0) gitlab-default_value_for (3.1.1) activerecord (>= 3.2.0, < 6.0) - gitlab-markup (1.6.5) + gitlab-markup (1.7.0) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) gitlab-styles (2.5.1) @@ -1020,7 +1020,7 @@ DEPENDENCIES gitaly-proto (~> 1.13.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) - gitlab-markup (~> 1.6.5) + gitlab-markup (~> 1.7.0) gitlab-sidekiq-fetcher (~> 0.4.0) gitlab-styles (~> 2.4) gitlab_omniauth-ldap (~> 2.1.1) diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000000..2afbe5fc3f2 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +docutils = "==0.13.1" + +[requires] +python_version = "3.4" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000000..dc23ca89ed2 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,30 @@ +{ + "_meta": { + "hash": { + "sha256": "ec82d5e7c10fd591aeebbc9b7b62d730f7fd70dc52e4e4818834891aa4194c73" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.4" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "docutils": { + "hashes": [ + "sha256:718c0f5fb677be0f34b781e04241c4067cbd9327b66bdd8e763201130f5175be", + "sha256:cb3ebcb09242804f84bdbf0b26504077a054da6772c6f4d625f335cc53ebf94d", + "sha256:de454f1015958450b72641165c08afe7023cd7e3944396448f2fb1b0ccba9d77" + ], + "index": "pypi", + "version": "==0.13.1" + } + }, + "develop": {} +} diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index fba30aea9ae..e5e1cbb1e62 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -16,3 +16,63 @@ $.fn.extend({ .removeClass('disabled'); }, }); + +/* + Starting with bootstrap 4.3.1, bootstrap sanitizes html used for tooltips / popovers. + This extends the default whitelists with more elements / attributes: + https://getbootstrap.com/docs/4.3/getting-started/javascript/#sanitizer + */ +const whitelist = $.fn.tooltip.Constructor.Default.whiteList; + +const inputAttributes = ['value', 'type']; + +const dataAttributes = [ + 'data-toggle', + 'data-placement', + 'data-container', + 'data-title', + 'data-class', + 'data-clipboard-text', + 'data-placement', +]; + +// Whitelisting data attributes +whitelist['*'] = [ + ...whitelist['*'], + ...dataAttributes, + 'title', + 'width height', + 'abbr', + 'datetime', + 'name', + 'width', + 'height', +]; + +// Whitelist missing elements: +whitelist.label = ['for']; +whitelist.button = [...inputAttributes]; +whitelist.input = [...inputAttributes]; + +whitelist.tt = []; +whitelist.samp = []; +whitelist.kbd = []; +whitelist.var = []; +whitelist.dfn = []; +whitelist.cite = []; +whitelist.big = []; +whitelist.address = []; +whitelist.dl = []; +whitelist.dt = []; +whitelist.dd = []; +whitelist.abbr = []; +whitelist.acronym = []; +whitelist.blockquote = []; +whitelist.del = []; +whitelist.ins = []; +whitelist['gl-emoji'] = []; + +// Whitelisting SVG tags and attributes +whitelist.svg = ['viewBox']; +whitelist.use = ['xlink:href']; +whitelist.path = ['d']; diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index a5125c3d077..d41d1464166 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -128,9 +128,6 @@ export default { isModeChanged() { return this.diffFile.viewer.name === diffViewerModes.mode_changed; }, - showExpandDiffToFullFileEnabled() { - return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded; - }, }, mounted() { polyfillSticky(this.$refs.header); @@ -258,7 +255,7 @@ export default { <icon name="external-link" /> </gl-button> <gl-button - v-if="showExpandDiffToFullFileEnabled" + v-if="!diffFile.is_fully_expanded" class="expand-file js-expand-file" @click="toggleFullDiff(diffFile.file_path)" > diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index ff4e16178e8..55613d815ce 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -99,7 +99,7 @@ export default { /> <div - v-if="shouldRenderDeployBoard" + v-if="shouldRenderDeployBoard(model)" :key="`deploy-board-row-${i}`" class="js-deploy-board-row" > diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 7883a3f9abc..ba6a17827f7 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; +import '~/vue_shared/mixins/is_ee'; import Flash from '../../flash'; import MonitoringService from '../services/monitoring_service'; import MonitorAreaChart from './charts/area.vue'; @@ -21,6 +22,7 @@ export default { GlDropdown, GlDropdownItem, }, + props: { hasMetrics: { type: Boolean, @@ -107,9 +109,29 @@ export default { } }, mounted() { + this.servicePromises = [ + this.service + .getGraphsData() + .then(data => this.store.storeMetrics(data)) + .catch(() => Flash(s__('Metrics|There was an error while retrieving metrics'))), + this.service + .getDeploymentData() + .then(data => this.store.storeDeploymentData(data)) + .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), + ]; if (!this.hasMetrics) { this.state = 'gettingStarted'; } else { + if (this.environmentsEndpoint) { + this.servicePromises.push( + this.service + .getEnvironmentsData() + .then(data => this.store.storeEnvironmentsData(data)) + .catch(() => + Flash(s__('Metrics|There was an error getting environments information.')), + ), + ); + } this.getGraphsData(); sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); sidebarMutationObserver.observe(document.querySelector('.layout-page'), { @@ -125,17 +147,7 @@ export default { }, getGraphsData() { this.state = 'loading'; - Promise.all([ - this.service.getGraphsData().then(data => this.store.storeMetrics(data)), - this.service - .getDeploymentData() - .then(data => this.store.storeDeploymentData(data)) - .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), - this.service - .getEnvironmentsData() - .then(data => this.store.storeEnvironmentsData(data)) - .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))), - ]) + Promise.all(this.servicePromises) .then(() => { if (this.store.groups.length < 1) { this.state = 'noData'; @@ -159,7 +171,7 @@ export default { <template> <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> - <div class="environments d-flex align-items-center"> + <div v-if="environmentsEndpoint" class="environments d-flex align-items-center"> <strong>{{ s__('Metrics|Environment') }}</strong> <gl-dropdown class="prepend-left-10 js-environments-dropdown" @@ -190,7 +202,17 @@ export default { :alert-data="getGraphAlerts(graphData.id)" :container-width="elWidth" group-id="monitor-area-chart" - /> + > + <alert-widget + v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData.id" + :alerts-endpoint="alertsEndpoint" + :label="getGraphLabel(graphData)" + :current-alerts="getQueryAlerts(graphData)" + :custom-metric-id="graphData.id" + :alert-data="alertData[graphData.id]" + @setAlerts="setAlerts" + /> + </monitor-area-chart> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index c5ae7e7ee10..b59ddd0d57a 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -20,12 +20,20 @@ export default { required: true, }, }, - data() { - return { - outputType: '', - }; - }, methods: { + outputType(output) { + if (output.text) { + return 'text/plain'; + } else if (output.data['image/png']) { + return 'image/png'; + } else if (output.data['text/html']) { + return 'text/html'; + } else if (output.data['image/svg+xml']) { + return 'image/svg+xml'; + } + + return 'text/plain'; + }, dataForType(output, type) { let data = output.data[type]; @@ -39,20 +47,13 @@ export default { if (output.text) { return CodeOutput; } else if (output.data['image/png']) { - this.outputType = 'image/png'; - return ImageOutput; } else if (output.data['text/html']) { - this.outputType = 'text/html'; - return HtmlOutput; } else if (output.data['image/svg+xml']) { - this.outputType = 'image/svg+xml'; - return HtmlOutput; } - this.outputType = 'text/plain'; return CodeOutput; }, rawCode(output) { @@ -60,7 +61,7 @@ export default { return output.text.join(''); } - return this.dataForType(output, this.outputType); + return this.dataForType(output, this.outputType(output)); }, }, }; @@ -73,7 +74,7 @@ export default { v-for="(output, index) in outputs" :key="index" type="output" - :output-type="outputType" + :output-type="outputType(output)" :count="count" :index="index" :raw-code="rawCode(output)" diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index e7fa05faa8a..6aed2492084 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -4,6 +4,7 @@ import Flash from './flash'; export default function notificationsDropdown() { $(document).on('click', '.update-notification', function updateNotificationCallback(e) { e.preventDefault(); + if ($(this).is('.is-active') && $(this).data('notificationLevel') === 'custom') { return; } diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js index 9177943f88a..dd79ade5bc9 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js @@ -1,6 +1,16 @@ +import Flash from '~/flash'; +import { __ } from '~/locale'; + export default { methods: { clickTriggeredByPipeline() {}, clickTriggeredPipeline() {}, + requestRefreshPipelineGraph() { + // When an action is clicked + // (wether in the dropdown or in the main nodes, we refresh the big graph) + this.mediator + .refreshPipeline() + .catch(() => Flash(__('An error occurred while making the request.'))); + }, }, }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 8adbd39edd4..6660f8120f8 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -29,15 +29,6 @@ export default () => { mediator, }; }, - methods: { - requestRefreshPipelineGraph() { - // When an action is clicked - // (wether in the dropdown or in the main nodes, we refresh the big graph) - this.mediator - .refreshPipeline() - .catch(() => Flash(__('An error occurred while making the request.'))); - }, - }, render(createElement) { return createElement('pipeline-graph', { props: { diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 052e34a8aef..259278b6410 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -1,7 +1,6 @@ export default class PipelineStore { constructor() { this.state = {}; - this.state.pipeline = {}; } diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 216877a4461..ab9047c54e4 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -60,6 +60,7 @@ @import 'framework/memory_graph'; @import 'framework/responsive_tables'; @import 'framework/stacked_progress_bar'; +@import 'framework/sortable'; @import 'framework/ci_variable_list'; @import 'framework/feature_highlight'; @import 'framework/terms'; diff --git a/app/assets/stylesheets/framework/sortable.scss b/app/assets/stylesheets/framework/sortable.scss new file mode 100644 index 00000000000..8c070200135 --- /dev/null +++ b/app/assets/stylesheets/framework/sortable.scss @@ -0,0 +1,92 @@ +.sortable-container { + background-color: $gray-light; + + .flex-list { + padding: 5px; + margin-bottom: 0; + } +} + +.sortable-row { + .flex-row { + display: flex; + + &.issuable-info-container { + padding-right: 0; + } + } + + .sortable-link { + color: $black; + } +} + +.gl-sortable { + .header { + user-select: none; + + &:hover { + cursor: pointer; + background-color: $gray-100; + } + + &:focus { + outline: 1px solid $blue-300; + } + } +} + +.related-issues-list-item { + .card-body, + .issuable-info-container { + padding: $gl-padding-4 $gl-padding-4 $gl-padding-4 $gl-padding; + + .block-truncated { + padding: $gl-padding-8 0; + line-height: $gl-btn-line-height; + } + + @include media-breakpoint-down(md) { + padding-left: $gl-padding; + + .block-truncated { + flex-direction: column-reverse; + padding: $gl-padding-4 0; + + .text-secondary { + margin-top: $gl-padding-4; + } + + .issue-token-title-text { + display: block; + } + } + + .issue-item-remove-button { + align-self: baseline; + } + } + + @include media-breakpoint-only(md) { + .block-truncated .issue-token-title-text { + white-space: nowrap; + } + + .issue-item-remove-button { + align-self: center; + } + } + + @include media-breakpoint-down(sm) { + padding-left: $gl-padding-8; + + .block-truncated .issue-token-title-text { + white-space: normal; + } + } + } + + &.is-dragging { + padding: 0; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index efebbd124d0..5d4c84c494d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -711,3 +711,11 @@ $mr-version-controls-height: 56px; Compare Branches */ $compare-branches-sticky-header-height: 68px; + +/** + Bootstrap 4.2.0 introduced new icons for validating forms. + Our design system does not use those, so we are disabling them for now: + - Docs: https://getbootstrap.com/docs/4.3/components/forms/#server-side + - Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242 + */ +$enable-validation-icons: false; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index efcc437bd7f..fb4d3f23cd9 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -9,7 +9,6 @@ $input-border-color: $gray-200; $input-color: $gl-text-color; $font-family-sans-serif: $regular-font; $font-family-monospace: $monospace-font; -$input-line-height: 20px; $btn-line-height: 20px; $table-accent-bg: $gray-light; $card-border-color: $border-color; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 81216b2b98e..ed0e9db035b 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -19,6 +19,7 @@ .is-ghost { opacity: 0.3; + pointer-events: none; } .dropdown-projects { @@ -50,6 +51,19 @@ .content-wrapper { padding-bottom: 0; } + + .issues-details-filters { + display: flex; + } + + .filter-form { + width: 100%; + } +} + +.board-extra-actions { + font-size: 0; + white-space: nowrap; } .boards-app { @@ -236,7 +250,8 @@ } } -.board-blank-state { +.board-blank-state, +.board-promotion-state { padding: $gl-padding; background-color: $white-light; flex: 1; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 61ecf133b02..0eb854ecf98 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -346,7 +346,9 @@ } > .popover-title, - > .popover-content { + > .popover-content, + > .popover-header, + > .popover-body { padding: 8px; font-size: 12px; white-space: nowrap; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9e2375b84d0..6415d902ca6 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -256,6 +256,10 @@ .selectbox { display: none; + + &.show { + display: block; + } } .btn-clipboard:hover { @@ -309,6 +313,7 @@ } .no-value, + .btn-default-hover-link, .btn-secondary-hover-link { color: $gl-text-color-secondary; } @@ -592,7 +597,6 @@ margin: -7px; } - .user-list { display: flex; flex-wrap: wrap; @@ -716,6 +720,7 @@ .issuable-main-info { flex: 1 auto; margin-right: 10px; + min-width: 0; .issue-weight-icon { vertical-align: sub; @@ -777,6 +782,7 @@ @media(max-width: map-get($grid-breakpoints, lg)-1) { .task-status, .issuable-due-date, + .issuable-weight, .project-ref-path { display: none; } diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index 5507328f8ae..d5c4712bd78 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -36,7 +36,7 @@ module AuthenticatesWithTwoFactor end def locked_user_redirect(user) - flash.now[:alert] = 'Invalid Login or password' + flash.now[:alert] = _('Invalid Login or password') render 'devise/sessions/new' end @@ -66,7 +66,7 @@ module AuthenticatesWithTwoFactor else user.increment_failed_attempts! Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP") - flash.now[:alert] = 'Invalid two-factor code.' + flash.now[:alert] = _('Invalid two-factor code.') prompt_for_two_factor(user) end end @@ -83,7 +83,7 @@ module AuthenticatesWithTwoFactor else user.increment_failed_attempts! Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F") - flash.now[:alert] = 'Authentication via U2F device failed.' + flash.now[:alert] = _('Authentication via U2F device failed.') prompt_for_two_factor(user) end end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index b3777fd2b0f..e8e681ce649 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -31,7 +31,7 @@ module CreatesCommit respond_to do |format| format.html { redirect_to success_path } - format.json { render json: { message: "success", filePath: success_path } } + format.json { render json: { message: _("success"), filePath: success_path } } end else flash[:alert] = result[:message] @@ -45,7 +45,7 @@ module CreatesCommit redirect_to failure_path end end - format.json { render json: { message: "failed", filePath: failure_path } } + format.json { render json: { message: _("failed"), filePath: failure_path } } end end end @@ -60,15 +60,22 @@ module CreatesCommit private def update_flash_notice(success_notice) - flash[:notice] = success_notice || "Your changes have been successfully committed." + flash[:notice] = success_notice || _("Your changes have been successfully committed.") if create_merge_request? - if merge_request_exists? - flash[:notice] = nil - else - target = different_project? ? "project" : "branch" - flash[:notice] = flash[:notice] + " You can now submit a merge request to get this change into the original #{target}." - end + flash[:notice] = + if merge_request_exists? + nil + else + mr_message = + if different_project? + _("You can now submit a merge request to get this change into the original project.") + else + _("You can now submit a merge request to get this change into the original branch.") + end + + flash[:notice] += " " + mr_message + end end end diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 57e444319e0..f7137a04437 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -26,7 +26,7 @@ module LfsRequest render( json: { - message: 'Git LFS is not enabled on this GitLab server, contact your admin.', + message: _('Git LFS is not enabled on this GitLab server, contact your admin.'), documentation_url: help_url }, status: :not_implemented @@ -51,7 +51,7 @@ module LfsRequest def render_lfs_forbidden render( json: { - message: 'Access forbidden. Check your access level.', + message: _('Access forbidden. Check your access level.'), documentation_url: help_url }, content_type: CONTENT_TYPE, @@ -62,7 +62,7 @@ module LfsRequest def render_lfs_not_found render( json: { - message: 'Not found.', + message: _('Not found.'), documentation_url: help_url }, content_type: CONTENT_TYPE, diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 6402e01ddc0..0b2756c0c6a 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -9,7 +9,7 @@ module MembershipActions result = Members::CreateService.new(current_user, create_params).execute(membershipable) if result[:status] == :success - redirect_to members_page_url, notice: 'Users were successfully added.' + redirect_to members_page_url, notice: _('Users were successfully added.') else redirect_to members_page_url, alert: result[:message] end @@ -35,9 +35,16 @@ module MembershipActions respond_to do |format| format.html do - source = source_type == 'group' ? 'group and any subresources' : source_type + message = + begin + case membershipable + when Namespace + _("User was successfully removed from group and any subresources.") + else + _("User was successfully removed from project.") + end + end - message = "User was successfully removed from #{source}." redirect_to members_page_url, notice: message end @@ -49,7 +56,7 @@ module MembershipActions membershipable.request_access(current_user) redirect_to polymorphic_path(membershipable), - notice: 'Your request for access has been queued for review.' + notice: _('Your request for access has been queued for review.') end def approve_access_request @@ -68,9 +75,9 @@ module MembershipActions notice = if member.request? - "Your access request to the #{source_type} has been withdrawn." + _("Your access request to the %{source_type} has been withdrawn.") % { source_type: source_type } else - "You left the \"#{membershipable.human_name}\" #{source_type}." + _("You left the \"%{membershipable_human_name}\" %{source_type}.") % { membershipable_human_name: membershipable.human_name, source_type: source_type } end respond_to do |format| @@ -90,9 +97,9 @@ module MembershipActions if member.invite? member.resend_invite - redirect_to members_page_url, notice: 'The invitation was successfully resent.' + redirect_to members_page_url, notice: _('The invitation was successfully resent.') else - redirect_to members_page_url, alert: 'The invitation has already been accepted.' + redirect_to members_page_url, alert: _('The invitation has already been accepted.') end end @@ -125,6 +132,16 @@ module MembershipActions end def source_type - @source_type ||= membershipable.class.to_s.humanize(capitalize: false) + @source_type ||= + begin + case membershipable + when Namespace + _("group") + when Project + _("project") + else + raise "Unknown membershipable type: #{membershipable}!" + end + end end end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index c3a1b12af84..a8ffa33f1c7 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -12,9 +12,9 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable_path, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully." + redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase } else - redirect_to spammable_path, alert: 'Error with Akismet. Please check the logs for more info.' + redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.') end end @@ -33,7 +33,7 @@ module SpammableActions ensure_spam_config_loaded! if params[:recaptcha_verification] - flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.') end respond_to do |format| diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 4ec0e94df9a..59f6d3452a3 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -16,7 +16,7 @@ module UploadsActions end else format.json do - render json: 'Invalid file.', status: :unprocessable_entity + render json: _('Invalid file.'), status: :unprocessable_entity end end end @@ -57,7 +57,7 @@ module UploadsActions render json: authorized rescue SocketError - render json: "Error uploading file", status: :internal_server_error + render json: _("Error uploading file"), status: :internal_server_error end private diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 3fa582cf25b..f173c263474 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -21,7 +21,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController format.html do redirect_to dashboard_todos_path, status: 302, - notice: 'Todo was successfully marked as done.' + notice: _('Todo was successfully marked as done.') end format.js { head :ok } format.json { render json: todos_counts } @@ -32,7 +32,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user) respond_to do |format| - format.html { redirect_to dashboard_todos_path, status: 302, notice: 'All todos were marked as done.' } + format.html { redirect_to dashboard_todos_path, status: 302, notice: _('All todos were marked as done.') } format.js { head :ok } format.json { render json: todos_counts.merge(updated_ids: updated_ids) } end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index e147d32be2e..7b5dc22815c 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -12,6 +12,7 @@ class GraphqlController < ApplicationController protect_from_forgery with: :null_session, only: :execute before_action :check_graphql_feature_flag! + before_action :authorize_access_api! before_action(only: [:execute]) { authenticate_sessionless_user!(:api) } def execute @@ -37,6 +38,10 @@ class GraphqlController < ApplicationController private + def authorize_access_api! + access_denied!("API not accessible for user.") unless can?(current_user, :access_api) + end + # Overridden from the ApplicationController to make the response look like # a GraphQL response. That is nicely picked up in Graphiql. def render_404 diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index dd8fbf7a029..f8e32451b02 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -16,7 +16,7 @@ class Groups::RunnersController < Groups::ApplicationController def update if Ci::UpdateRunnerService.new(@runner).update(runner_params) - redirect_to group_runner_path(@group, @runner), notice: 'Runner was successfully updated.' + redirect_to group_runner_path(@group, @runner), notice: _('Runner was successfully updated.') else render 'edit' end @@ -30,17 +30,17 @@ class Groups::RunnersController < Groups::ApplicationController def resume if Ci::UpdateRunnerService.new(@runner).update(active: true) - redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.' + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: _('Runner was successfully updated.') else - redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.' + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: _('Runner was not updated.') end end def pause if Ci::UpdateRunnerService.new(@runner).update(active: false) - redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.' + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: _('Runner was successfully updated.') else - redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.' + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: _('Runner was not updated.') end end diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index f378f7ac79a..c465e622de0 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -13,7 +13,7 @@ module Groups def reset_registration_token @group.reset_runners_token! - flash[:notice] = 'New runners registration token has been generated!' + flash[:notice] = _('GroupSettings|New runners registration token has been generated!') redirect_to group_settings_ci_cd_path end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 2b1395f364f..293d76ea765 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -62,7 +62,7 @@ class Import::BitbucketController < Import::BaseController render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else - render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity + render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity end end diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index f333e43b892..643a3bfed1f 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -25,7 +25,7 @@ class Import::BitbucketServerController < Import::BaseController repo = bitbucket_client.repo(@project_key, @repo_slug) unless repo - return render json: { errors: "Project #{@project_key}/#{@repo_slug} could not be found" }, status: :unprocessable_entity + return render json: { errors: _("Project %{project_repo} could not be found") % { project_repo: "#{@project_key}/#{@repo_slug}" } }, status: :unprocessable_entity end project_name = params[:new_name].presence || repo.name @@ -41,10 +41,10 @@ class Import::BitbucketServerController < Import::BaseController render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else - render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity + render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity end - rescue BitbucketServer::Connection::ConnectionError => e - render json: { errors: "Unable to connect to server: #{e}" }, status: :unprocessable_entity + rescue BitbucketServer::Connection::ConnectionError => error + render json: { errors: _("Unable to connect to server: %{error}") % { error: error } }, status: :unprocessable_entity end def configure @@ -65,8 +65,8 @@ class Import::BitbucketServerController < Import::BaseController already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) } - rescue BitbucketServer::Connection::ConnectionError => e - flash[:alert] = "Unable to connect to server: #{e}" + rescue BitbucketServer::Connection::ConnectionError => error + flash[:alert] = _("Unable to connect to server: %{error}") % { error: error } clear_session_data redirect_to new_import_bitbucket_server_path end diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 5a439e6de78..a37ba682b91 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -14,7 +14,7 @@ class Import::FogbugzController < Import::BaseController res = Gitlab::FogbugzImport::Client.new(import_params.symbolize_keys) rescue # If the URI is invalid various errors can occur - return redirect_to new_import_fogbugz_path, alert: 'Could not connect to FogBugz, check your URL' + return redirect_to new_import_fogbugz_path, alert: _('Could not connect to FogBugz, check your URL') end session[:fogbugz_token] = res.get_token session[:fogbugz_uri] = params[:uri] @@ -29,14 +29,14 @@ class Import::FogbugzController < Import::BaseController user_map = params[:users] unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? } - flash.now[:alert] = 'All users must have a name.' + flash.now[:alert] = _('All users must have a name.') return render 'new_user_map' end session[:fogbugz_user_map] = user_map - flash[:notice] = 'The user map has been saved. Continue by selecting the projects you want to import.' + flash[:notice] = _('The user map has been saved. Continue by selecting the projects you want to import.') redirect_to status_import_fogbugz_path end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 68ad8650dba..a23b2f8139e 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -46,7 +46,7 @@ class Import::GiteaController < Import::GithubController def provider_auth if session[access_token_key].blank? || provider_url.blank? redirect_to new_import_gitea_url, - alert: 'You need to specify both an Access Token and a Host URL.' + alert: _('You need to specify both an Access Token and a Host URL.') end end diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 498de0b07b8..5ec8e9e6fc5 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -42,7 +42,7 @@ class Import::GitlabController < Import::BaseController render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else - render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity + render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity end end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 354fba5d204..89889141be6 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -13,7 +13,7 @@ class Import::GitlabProjectsController < Import::BaseController def create unless file_is_valid? - return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive (ending in .gz)." }) + return redirect_back_or_default(options: { alert: _("You need to upload a GitLab project export archive (ending in .gz).") }) end @project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute @@ -21,7 +21,7 @@ class Import::GitlabProjectsController < Import::BaseController if @project.saved? redirect_to( project_path(@project), - notice: "Project '#{@project.name}' is being imported." + notice: _("Project '%{project_name}' is being imported.") % { project_name: @project.name } ) else redirect_back_or_default(options: { alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" }) diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 331f06c3dd6..4dddfbcd20d 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -11,18 +11,18 @@ class Import::GoogleCodeController < Import::BaseController dump_file = params[:dump_file] unless dump_file.respond_to?(:read) - return redirect_back_or_default(options: { alert: "You need to upload a Google Takeout archive." }) + return redirect_back_or_default(options: { alert: _("You need to upload a Google Takeout archive.") }) end begin dump = JSON.parse(dump_file.read) rescue - return redirect_back_or_default(options: { alert: "The uploaded file is not a valid Google Takeout archive." }) + return redirect_back_or_default(options: { alert: _("The uploaded file is not a valid Google Takeout archive.") }) end client = Gitlab::GoogleCodeImport::Client.new(dump) unless client.valid? - return redirect_back_or_default(options: { alert: "The uploaded file is not a valid Google Takeout archive." }) + return redirect_back_or_default(options: { alert: _("The uploaded file is not a valid Google Takeout archive.") }) end session[:google_code_dump] = dump @@ -44,13 +44,13 @@ class Import::GoogleCodeController < Import::BaseController begin user_map = JSON.parse(user_map_json) rescue - flash.now[:alert] = "The entered user map is not a valid JSON user map." + flash.now[:alert] = _("The entered user map is not a valid JSON user map.") return render "new_user_map" end unless user_map.is_a?(Hash) && user_map.all? { |k, v| k.is_a?(String) && v.is_a?(String) } - flash.now[:alert] = "The entered user map is not a valid JSON user map." + flash.now[:alert] = _("The entered user map is not a valid JSON user map.") return render "new_user_map" end @@ -62,7 +62,7 @@ class Import::GoogleCodeController < Import::BaseController session[:google_code_user_map] = user_map - flash[:notice] = "The user map has been saved. Continue by selecting the projects you want to import." + flash[:notice] = _("The user map has been saved. Continue by selecting the projects you want to import.") redirect_to status_import_google_code_path end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 0a33856a8d3..909b17e9c8d 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -29,7 +29,7 @@ class Projects::BlobController < Projects::ApplicationController end def create - create_commit(Files::CreateService, success_notice: "The file has been successfully created.", + create_commit(Files::CreateService, success_notice: _("The file has been successfully created."), success_path: -> { project_blob_path(@project, File.join(@branch_name, @file_path)) }, failure_view: :new, failure_path: project_new_blob_path(@project, @ref)) @@ -81,7 +81,7 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", + create_commit(Files::DeleteService, success_notice: _("The file has been successfully deleted."), success_path: -> { after_delete_path }, failure_view: :show, failure_path: project_blob_path(@project, @id)) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 32b7f3207ef..6ff2e222489 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -115,7 +115,7 @@ class Projects::BranchesController < Projects::ApplicationController DeleteMergedBranchesService.new(@project, current_user).async_execute redirect_to project_branches_path(@project), - notice: 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.' + notice: _('Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.') end private @@ -143,7 +143,7 @@ class Projects::BranchesController < Projects::ApplicationController def redirect_for_legacy_index_sort_or_search # Normalize a legacy URL with redirect if request.format != :json && !params[:state].presence && [:sort, :search, :page].any? { |key| params[key].presence } - redirect_to project_branches_filtered_path(@project, state: 'all'), notice: 'Update your bookmarked URLs as filtered/sorted branches URL has been changed.' + redirect_to project_branches_filtered_path(@project, state: 'all'), notice: _('Update your bookmarked URLs as filtered/sorted branches URL has been changed.') end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 6824a07dc76..514b03e23b5 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -38,7 +38,7 @@ class Projects::DeployKeysController < Projects::ApplicationController def update if deploy_key.update(update_params) - flash[:notice] = 'Deploy key was successfully updated.' + flash[:notice] = _('Deploy key was successfully updated.') redirect_to_repository_settings(@project, anchor: 'js-deploy-keys-settings') else render 'edit' diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index d439db97252..55d5fce9214 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController end def parse_repo_path - @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") + @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") end def render_missing_personal_access_token @@ -89,13 +89,19 @@ class Projects::GitHttpClientController < Projects::ApplicationController end def repository - wiki? ? project.wiki.repository : project.repository + repo_type.repository_for(project) end def wiki? - parse_repo_path unless defined?(@wiki) + repo_type.wiki? + end - @wiki + def repo_type + parse_repo_path unless defined?(@repo_type) + # When there a project did not exist, the parsed repo_type would be empty. + # In that case, we want to continue with a regular project repository. As we + # could create the project if the user pushing is allowed to do so. + @repo_type || Gitlab::GlRepository::PROJECT end def handle_basic_authentication(login, password) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index f28af42d1b7..e519cc1f158 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -55,7 +55,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController def render_ok set_workhorse_internal_api_content_type - render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name) + render json: Gitlab::Workhorse.git_http_ok(repository, repo_type, user, action_name) end def render_403(exception) @@ -99,7 +99,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def access_klass - @access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess + @access_klass ||= repo_type.access_checker_class end def project_path diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index bc942ba9288..dc65f9959db 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -18,7 +18,7 @@ class Projects::GroupLinksController < Projects::ApplicationController flash[:alert] = result[:message] if result[:http_status] == 409 else - flash[:alert] = 'Please select a group.' + flash[:alert] = _('Please select a group.') end redirect_to project_project_members_path(project) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index bc84418b79f..5fa0339f44d 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -32,7 +32,7 @@ class Projects::HooksController < Projects::ApplicationController def update if hook.update(hook_params) - flash[:notice] = 'Hook was successfully updated.' + flash[:notice] = _('Hook was successfully updated.') redirect_to project_settings_integrations_path(@project) else render 'edit' diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 8b33fa85c1e..8ee0bd26daf 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -42,9 +42,9 @@ class Projects::ImportsController < Projects::ApplicationController def finished_notice if @project.forked? - 'The project was successfully forked.' + _('The project was successfully forked.') else - 'The project was successfully imported.' + _('The project was successfully imported.') end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b9d02a62fc3..e9ed5554ab4 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -95,9 +95,9 @@ class Projects::IssuesController < Projects::ApplicationController if service.discussions_to_resolve.count(&:resolved?) > 0 flash[:notice] = if service.discussion_to_resolve_id - "Resolved 1 discussion." + _("Resolved 1 discussion.") else - "Resolved all discussions." + _("Resolved all discussions.") end end diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index d5ce790e2d9..35cc32d3e63 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -122,7 +122,7 @@ class Projects::JobsController < Projects::ApplicationController def erase if @build.erase(erased_by: current_user) redirect_to project_job_path(project, @build), - notice: "Job has been successfully erased!" + notice: _("Job has been successfully erased!") else respond_422 end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 640038818f2..386a1f00bd2 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -132,7 +132,7 @@ class Projects::LabelsController < Projects::ApplicationController respond_to do |format| format.html do redirect_to(project_labels_path(@project), - notice: 'Failed to promote label due to internal error. Please contact administrators.') + notice: _('Failed to promote label due to internal error. Please contact administrators.')) end format.js end diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index be40077d389..42c415757f9 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -26,7 +26,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController def deprecated render( json: { - message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', + message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'), documentation_url: "#{Gitlab.config.gitlab.url}/help" }, status: :not_implemented @@ -62,7 +62,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController else object[:error] = { code: 404, - message: "Object does not exist on the server or you don't have permissions to access it" + message: _("Object does not exist on the server or you don't have permissions to access it") } end end diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb index 045a4e974fe..011ac9a42f8 100644 --- a/app/controllers/projects/merge_requests/conflicts_controller.rb +++ b/app/controllers/projects/merge_requests/conflicts_controller.rb @@ -16,12 +16,12 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap render json: @conflicts_list elsif @merge_request.can_be_merged? render json: { - message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.', + message: _('The merge conflicts for this merge request have already been resolved. Please return to the merge request.'), type: 'error' } else render json: { - message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.', + message: _('The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.'), type: 'error' } end @@ -43,7 +43,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap return render_404 unless @conflicts_list.can_be_resolved_in_ui? if @merge_request.can_be_merged? - render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' } + render status: :bad_request, json: { message: _('The merge conflicts for this merge request have already been resolved.') } return end @@ -52,7 +52,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap .new(merge_request) .execute(current_user, params) - flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' + flash[:notice] = _('All merge conflicts were resolved. The merge request can now be merged.') render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) } rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2b78abc66df..5cf7fa3422d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -16,10 +16,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] - before_action only: [:show] do - push_frontend_feature_flag(:expand_diff_full_file) - end - def index @merge_requests = @issuables diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb index ab7ab13657a..ef330ae00f4 100644 --- a/app/controllers/projects/mirrors_controller.rb +++ b/app/controllers/projects/mirrors_controller.rb @@ -18,7 +18,7 @@ class Projects::MirrorsController < Projects::ApplicationController result = ::Projects::UpdateService.new(project, current_user, mirror_params).execute if result[:status] == :success - flash[:notice] = 'Mirroring settings were successfully updated.' + flash[:notice] = _('Mirroring settings were successfully updated.') else flash[:alert] = project.errors.full_messages.join(', ').html_safe end @@ -38,7 +38,7 @@ class Projects::MirrorsController < Projects::ApplicationController def update_now if params[:sync_remote] project.update_remote_mirrors - flash[:notice] = "The remote repository is being updated..." + flash[:notice] = _("The remote repository is being updated...") end redirect_to_repository_settings(project, anchor: 'js-push-remote-settings') diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index acf56f0eb6a..fd5b89298a7 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -50,9 +50,11 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController job_id = RunPipelineScheduleWorker.perform_async(schedule.id, current_user.id) if job_id - flash[:notice] = "Successfully scheduled a pipeline to run. Go to the <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details.".html_safe + link_to_pipelines = view_context.link_to(_('Pipelines page'), project_pipelines_path(@project)) + message = _("Successfully scheduled a pipeline to run. Go to the %{link_to_pipelines} for details.").html_safe % { link_to_pipelines: link_to_pipelines } + flash[:notice] = message.html_safe else - flash[:alert] = 'Unable to schedule a pipeline to run immediately' + flash[:alert] = _('Unable to schedule a pipeline to run immediately') end redirect_to pipeline_schedules_path(@project) @@ -85,7 +87,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController return unless limiter.throttled?([current_user, schedule], 1) - flash[:alert] = 'You cannot play this scheduled pipeline at the moment. Please wait a minute.' + flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.') redirect_to pipeline_schedules_path(@project) end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 91f40b90aa8..ca62f54813b 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -15,7 +15,7 @@ class Projects::RunnersController < Projects::ApplicationController def update if Ci::UpdateRunnerService.new(@runner).update(runner_params) - redirect_to project_runner_path(@project, @runner), notice: 'Runner was successfully updated.' + redirect_to project_runner_path(@project, @runner), notice: _('Runner was successfully updated.') else render 'edit' end @@ -31,17 +31,17 @@ class Projects::RunnersController < Projects::ApplicationController def resume if Ci::UpdateRunnerService.new(@runner).update(active: true) - redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.' + redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.') else - redirect_to project_runners_path(@project), alert: 'Runner was not updated.' + redirect_to project_runners_path(@project), alert: _('Runner was not updated.') end end def pause if Ci::UpdateRunnerService.new(@runner).update(active: false) - redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.' + redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.') else - redirect_to project_runners_path(@project), alert: 'Runner was not updated.' + redirect_to project_runners_path(@project), alert: _('Runner was not updated.') end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index f1c9d0d0f77..e0df51590ae 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -43,20 +43,20 @@ class Projects::ServicesController < Projects::ApplicationController if outcome[:success] {} else - { error: true, message: 'Test failed.', service_response: outcome[:result].to_s, test_failed: true } + { error: true, message: _('Test failed.'), service_response: outcome[:result].to_s, test_failed: true } end else - { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(','), test_failed: false } + { error: true, message: _('Validations failed.'), service_response: @service.errors.full_messages.join(','), test_failed: false } end rescue Gitlab::HTTP::BlockedUrlError => e - { error: true, message: 'Test failed.', service_response: e.message, test_failed: true } + { error: true, message: _('Test failed.'), service_response: e.message, test_failed: true } end def success_message if @service.active? - "#{@service.title} activated." + _("%{service_title} activated.") % { service_title: @service.title } else - "#{@service.title} settings saved, but not activated." + _("%{service_title} settings saved, but not activated.") % { service_title: @service.title } end end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index f2f63e986bb..d1c5cef76fa 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -13,7 +13,7 @@ module Projects Projects::UpdateService.new(project, current_user, update_params).tap do |service| result = service.execute if result[:status] == :success - flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." + flash[:notice] = _("Pipelines settings for '%{project_name}' were successfully updated.") % { project_name: @project.name } run_autodevops_pipeline(service) @@ -39,7 +39,7 @@ module Projects def reset_registration_token @project.reset_runners_token! - flash[:notice] = 'New runners registration token has been generated!' + flash[:notice] = _('New runners registration token has been generated!') redirect_to namespace_project_settings_ci_cd_path end @@ -58,7 +58,7 @@ module Projects return unless service.run_auto_devops_pipeline? if @project.empty_repo? - flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch." + flash[:warning] = _("This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch.") return end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index edebfc55c17..90d53aa08ea 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -37,7 +37,7 @@ class Projects::TreeController < Projects::ApplicationController def create_dir return render_404 unless @commit_params.values.all? - create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", + create_commit(Files::CreateDirService, success_notice: _("The directory has been successfully created."), success_path: project_tree_path(@project, File.join(@branch_name, @dir_name)), failure_path: project_tree_path(@project, @ref)) end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index c7b4ebb2b24..284e119ca06 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -16,9 +16,9 @@ class Projects::TriggersController < Projects::ApplicationController @trigger = project.triggers.create(trigger_params.merge(owner: current_user)) if @trigger.valid? - flash[:notice] = 'Trigger was created successfully.' + flash[:notice] = _('Trigger was created successfully.') else - flash[:alert] = 'You could not create a new trigger.' + flash[:alert] = _('You could not create a new trigger.') end redirect_to project_settings_ci_cd_path(@project, anchor: 'js-pipeline-triggers') @@ -26,9 +26,9 @@ class Projects::TriggersController < Projects::ApplicationController def take_ownership if trigger.update(owner: current_user) - flash[:notice] = 'Trigger was re-assigned.' + flash[:notice] = _('Trigger was re-assigned.') else - flash[:alert] = 'You could not take ownership of trigger.' + flash[:alert] = _('You could not take ownership of trigger.') end redirect_to project_settings_ci_cd_path(@project, anchor: 'js-pipeline-triggers') @@ -39,7 +39,7 @@ class Projects::TriggersController < Projects::ApplicationController def update if trigger.update(trigger_params) - redirect_to project_settings_ci_cd_path(@project, anchor: 'js-pipeline-triggers'), notice: 'Trigger was successfully updated.' + redirect_to project_settings_ci_cd_path(@project, anchor: 'js-pipeline-triggers'), notice: _('Trigger was successfully updated.') else render action: "edit" end @@ -47,9 +47,9 @@ class Projects::TriggersController < Projects::ApplicationController def destroy if trigger.destroy - flash[:notice] = "Trigger removed." + flash[:notice] = _("Trigger removed.") else - flash[:alert] = "Could not remove the trigger." + flash[:alert] = _("Could not remove the trigger.") end redirect_to project_settings_ci_cd_path(@project, anchor: 'js-pipeline-triggers'), status: :found diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 88dd111132b..da2420633ef 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -49,7 +49,7 @@ class Projects::WikisController < Projects::ApplicationController if @page.valid? redirect_to( project_wiki_path(@project, @page), - notice: 'Wiki was successfully updated.' + notice: _('Wiki was successfully updated.') ) else render 'edit' @@ -65,7 +65,7 @@ class Projects::WikisController < Projects::ApplicationController if @page.persisted? redirect_to( project_wiki_path(@project, @page), - notice: 'Wiki was successfully updated.' + notice: _('Wiki was successfully updated.') ) else render action: "edit" @@ -85,7 +85,7 @@ class Projects::WikisController < Projects::ApplicationController else redirect_to( project_wiki_path(@project, :home), - notice: "Page not found" + notice: _("Page not found") ) end end @@ -95,7 +95,7 @@ class Projects::WikisController < Projects::ApplicationController redirect_to project_wiki_path(@project, :home), status: 302, - notice: "Page was successfully deleted" + notice: _("Page was successfully deleted") rescue Gitlab::Git::Wiki::OperationError => e @error = e render 'edit' @@ -118,7 +118,7 @@ class Projects::WikisController < Projects::ApplicationController @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15)) end rescue ProjectWiki::CouldNotCreateWikiError - flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." + flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.") redirect_to project_path(@project) false end @@ -155,7 +155,7 @@ class Projects::WikisController < Projects::ApplicationController end def set_encoding_error - flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." + flash.now[:notice] = _("The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.") end def file_blob diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 93d3c991846..0319e95d439 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -81,7 +81,7 @@ class ProjectsFinder < UnionFinder if private_only? current_user.authorized_projects else - Project.public_or_visible_to_user(current_user) + Project.public_or_visible_to_user(current_user, params[:visibility_level]) end end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8110377850b..09165979b26 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -33,10 +33,15 @@ module SearchHelper "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\"" end - def find_project_for_result_blob(result) + def find_project_for_result_blob(projects, result) @project end + # Used in EE + def blob_projects(results) + nil + end + def parse_search_result(result) result end diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 347c3c8c37f..8afd548f3e3 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Knative < ActiveRecord::Base - VERSION = '0.2.2'.freeze + VERSION = '0.3.0'.freeze REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze FETCH_IP_ADDRESS_DELAY = 30.seconds @@ -86,7 +86,7 @@ module Clusters end def ingress_service - cluster.kubeclient.get_service('knative-ingressgateway', 'istio-system') + cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system') end def services_for(ns: namespace) diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 87755cf3f3d..feabea9b8ba 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -77,8 +77,8 @@ class DiffNote < Note def supports_suggestion? return false unless noteable.supports_suggestion? && on_text? # We don't want to trigger side-effects of `diff_file` call. - return false unless file = fetch_diff_file - return false unless line = file.line_for_position(self.original_position) + return false unless file = latest_diff_file + return false unless line = file.line_for_position(self.position) line&.suggestible? end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 2c9e1ba1d80..510f856087d 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -14,6 +14,8 @@ class GroupMember < Member scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) } + scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count } + after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2c0692e1b45..19557fd476e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -66,6 +66,7 @@ class MergeRequest < ActiveRecord::Base has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline' + has_many :suggestions, through: :notes has_many :merge_request_assignees # Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457 diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 98db1bf7de7..2143571d1d4 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -12,6 +12,10 @@ class MergeRequestDiff < ActiveRecord::Base # Don't display more than 100 commits at once COMMITS_SAFE_SIZE = 100 + # Applies to closed or merged MRs when determining whether to migrate their + # diffs to external storage + EXTERNAL_DIFF_CUTOFF = 7.days.freeze + belongs_to :merge_request manual_inverse_association :merge_request, :merge_request_diff @@ -48,6 +52,81 @@ class MergeRequestDiff < ActiveRecord::Base end scope :recent, -> { order(id: :desc).limit(100) } + scope :files_in_database, -> { where(stored_externally: [false, nil]) } + + scope :not_latest_diffs, -> do + merge_requests = MergeRequest.arel_table + mr_diffs = arel_table + + join_condition = merge_requests[:id].eq(mr_diffs[:merge_request_id]) + .and(mr_diffs[:id].not_eq(merge_requests[:latest_merge_request_diff_id])) + + arel_join = mr_diffs.join(merge_requests).on(join_condition) + joins(arel_join.join_sources) + end + + scope :old_merged_diffs, -> (before) do + merge_requests = MergeRequest.arel_table + mr_metrics = MergeRequest::Metrics.arel_table + mr_diffs = arel_table + + mr_join = mr_diffs + .join(merge_requests) + .on(mr_diffs[:merge_request_id].eq(merge_requests[:id])) + + metrics_join_condition = mr_diffs[:merge_request_id] + .eq(mr_metrics[:merge_request_id]) + .and(mr_metrics[:merged_at].not_eq(nil)) + + metrics_join = mr_diffs.join(mr_metrics).on(metrics_join_condition) + + condition = MergeRequest.arel_table[:state].eq(:merged) + .and(MergeRequest::Metrics.arel_table[:merged_at].lteq(before)) + .and(MergeRequest::Metrics.arel_table[:merged_at].not_eq(nil)) + + joins(metrics_join.join_sources, mr_join.join_sources).where(condition) + end + + scope :old_closed_diffs, -> (before) do + condition = MergeRequest.arel_table[:state].eq(:closed) + .and(MergeRequest::Metrics.arel_table[:latest_closed_at].lteq(before)) + + joins(merge_request: :metrics).where(condition) + end + + def self.ids_for_external_storage_migration(limit:) + # No point doing any work unless the feature is enabled + return [] unless Gitlab.config.external_diffs.enabled + + case Gitlab.config.external_diffs.when + when 'always' + files_in_database.limit(limit).pluck(:id) + when 'outdated' + # Outdated is too complex to be a single SQL query, so split into three + before = EXTERNAL_DIFF_CUTOFF.ago + + ids = files_in_database + .old_merged_diffs(before) + .limit(limit) + .pluck(:id) + + return ids if ids.size >= limit + + ids += files_in_database + .old_closed_diffs(before) + .limit(limit - ids.size) + .pluck(:id) + + return ids if ids.size >= limit + + ids + files_in_database + .not_latest_diffs + .limit(limit - ids.size) + .pluck(:id) + else + [] + end + end mount_uploader :external_diff, ExternalDiffUploader @@ -55,7 +134,7 @@ class MergeRequestDiff < ActiveRecord::Base # It allows you to override variables like head_commit_sha before getting diff. after_create :save_git_content, unless: :importing? - after_save :update_external_diff_store, if: :external_diff_changed? + after_save :update_external_diff_store, if: -> { !importing? && external_diff_changed? } def self.find_by_diff_refs(diff_refs) find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha) @@ -294,6 +373,23 @@ class MergeRequestDiff < ActiveRecord::Base end end + # Transactionally migrate the current merge_request_diff_files entries to + # external storage. If external storage isn't an option for this diff, the + # method is a no-op. + def migrate_files_to_external_storage! + return if stored_externally? || !use_external_diff? || merge_request_diff_files.count == 0 + + rows = build_merge_request_diff_files(merge_request_diff_files) + + transaction do + MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all + create_merge_request_diff_files(rows) + save! + end + + merge_request_diff_files.reload + end + private def encode_in_base64?(diff_text) @@ -301,20 +397,7 @@ class MergeRequestDiff < ActiveRecord::Base diff_text.include?("\0") end - def create_merge_request_diff_files(diffs) - rows = - if has_attribute?(:external_diff) && Gitlab.config.external_diffs.enabled - build_external_merge_request_diff_files(diffs) - else - build_merge_request_diff_files(diffs) - end - - # Faster inserts - Gitlab::Database.bulk_insert('merge_request_diff_files', rows) - end - - def build_external_merge_request_diff_files(diffs) - rows = build_merge_request_diff_files(diffs) + def build_external_merge_request_diff_files(rows) tempfile = build_external_diff_tempfile(rows) self.external_diff = tempfile @@ -325,16 +408,21 @@ class MergeRequestDiff < ActiveRecord::Base tempfile&.unlink end + def create_merge_request_diff_files(rows) + rows = build_external_merge_request_diff_files(rows) if use_external_diff? + + # Faster inserts + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) + end + def build_external_diff_tempfile(rows) Tempfile.open(external_diff.filename) do |file| - rows.inject(0) do |offset, row| + rows.each do |row| data = row.delete(:diff) - row[:external_diff_offset] = offset - row[:external_diff_size] = data.size + row[:external_diff_offset] = file.pos + row[:external_diff_size] = data.bytesize file.write(data) - - offset + data.size end file @@ -361,6 +449,47 @@ class MergeRequestDiff < ActiveRecord::Base end end + def use_external_diff? + return false unless has_attribute?(:external_diff) + return false unless Gitlab.config.external_diffs.enabled + + case Gitlab.config.external_diffs.when + when 'always' + true + when 'outdated' + outdated_by_merge? || outdated_by_closure? || old_version? + else + false # Disable external diffs if misconfigured + end + end + + def outdated_by_merge? + return false unless merge_request&.metrics&.merged_at + + merge_request.merged? && merge_request.metrics.merged_at < EXTERNAL_DIFF_CUTOFF.ago + end + + def outdated_by_closure? + return false unless merge_request&.metrics&.latest_closed_at + + merge_request.closed? && merge_request.metrics.latest_closed_at < EXTERNAL_DIFF_CUTOFF.ago + end + + # We can't rely on `merge_request.latest_merge_request_diff_id` because that + # may have been changed in `save_git_content` without being reflected in + # the association's instance. This query is always subject to races, but + # the worst case is that we *don't* make a diff external when we could. The + # background worker will make it external at a later date. + def old_version? + latest_id = MergeRequest + .where(id: merge_request_id) + .limit(1) + .pluck(:latest_merge_request_diff_id) + .first + + self.id != latest_id + end + def load_diffs(options) # Ensure all diff files operate on the same external diff file instance if # present. This reduces file open/close overhead. @@ -394,7 +523,8 @@ class MergeRequestDiff < ActiveRecord::Base if diff_collection.any? new_attributes[:state] = :collected - create_merge_request_diff_files(diff_collection) + rows = build_merge_request_diff_files(diff_collection) + create_merge_request_diff_files(rows) end # Set our state to 'overflow' to make the #empty? and #collected? diff --git a/app/models/project.rb b/app/models/project.rb index 611c64c8f49..06010409574 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -459,14 +459,41 @@ class Project < ActiveRecord::Base # Returns a collection of projects that is either public or visible to the # logged in user. - def self.public_or_visible_to_user(user = nil) - if user - where('EXISTS (?) OR projects.visibility_level IN (?)', - user.authorizations_for_projects, - Gitlab::VisibilityLevel.levels_for_user(user)) - else - public_to_user - end + # + # requested_visiblity_levels: Normally all projects that are visible + # to the user (e.g. internal and public) are queried, but this + # parameter allows the caller to narrow the search space to optimize + # database queries. For instance, a caller may only want to see + # internal projects. Instead of querying for internal and public + # projects and throwing away public projects, this parameter allows + # the query to be targeted for only internal projects. + def self.public_or_visible_to_user(user = nil, requested_visibility_levels = []) + return public_to_user unless user + + visible_levels = Gitlab::VisibilityLevel.levels_for_user(user) + include_private = true + requested_visibility_levels = Array(requested_visibility_levels) + + if requested_visibility_levels.present? + visible_levels &= requested_visibility_levels + include_private = requested_visibility_levels.include?(Gitlab::VisibilityLevel::PRIVATE) + end + + public_or_internal_rel = + if visible_levels.present? + where('projects.visibility_level IN (?)', visible_levels) + else + Project.none + end + + private_rel = + if include_private + where('EXISTS (?)', user.authorizations_for_projects) + else + Project.none + end + + public_or_internal_rel.or(private_rel) end # project features may be "disabled", "internal", "enabled" or "public". If "internal", @@ -2003,12 +2030,8 @@ class Project < ActiveRecord::Base @storage = nil if storage_version_changed? end - def gl_repository(is_wiki:) - Gitlab::GlRepository.gl_repository(self, is_wiki) - end - - def reference_counter(wiki: false) - Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki)) + def reference_counter(type: Gitlab::GlRepository::PROJECT) + Gitlab::ReferenceCounter.new(type.identifier_for_subject(self)) end def badges @@ -2152,7 +2175,7 @@ class Project < ActiveRecord::Base end def wiki_reference_count - reference_counter(wiki: true).value + reference_counter(type: Gitlab::GlRepository::WIKI).value end def check_repository_absence! diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 6ea0716c192..268706a6aea 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -59,7 +59,7 @@ class ProjectWiki # Returns the Gitlab::Git::Wiki object. def wiki @wiki ||= begin - gl_repository = Gitlab::GlRepository.gl_repository(project, true) + gl_repository = Gitlab::GlRepository::WIKI.identifier_for_subject(project) raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository, full_path) create_repo!(raw_repository) unless raw_repository.exists? @@ -151,7 +151,7 @@ class ProjectWiki end def repository - @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true) + @repository ||= Repository.new(full_path, @project, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI) end def default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index ff355295862..574ce12b309 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -19,7 +19,7 @@ class Repository include Gitlab::RepositoryCacheAdapter - attr_accessor :full_path, :disk_path, :project, :is_wiki + attr_accessor :full_path, :disk_path, :project, :repo_type delegate :ref_name_for_sha, to: :raw_repository delegate :bundle_to_disk, to: :raw_repository @@ -60,12 +60,12 @@ class Repository xcode_config: :xcode_project? }.freeze - def initialize(full_path, project, disk_path: nil, is_wiki: false) + def initialize(full_path, project, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT) @full_path = full_path @disk_path = disk_path || full_path @project = project @commit_cache = {} - @is_wiki = is_wiki + @repo_type = repo_type end def ==(other) @@ -1112,7 +1112,7 @@ class Repository def initialize_raw_repository Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', - Gitlab::GlRepository.gl_repository(project, is_wiki), + repo_type.identifier_for_subject(project), project.full_path) end end diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index 09034646bff..22e2f11230d 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -1,11 +1,19 @@ # frozen_string_literal: true class Suggestion < ApplicationRecord + include Suggestible + belongs_to :note, inverse_of: :suggestions validates :note, presence: true validates :commit_id, presence: true, if: :applied? - delegate :original_position, :position, :noteable, to: :note + delegate :position, :noteable, to: :note + + scope :active, -> { where(outdated: false) } + + def diff_file + note.latest_diff_file + end def project noteable.source_project @@ -19,37 +27,37 @@ class Suggestion < ApplicationRecord position.file_path end - # For now, suggestions only serve as a way to send patches that - # will change a single line (being able to apply multiple in the same place), - # which explains `from_line` and `to_line` being the same line. - # We'll iterate on that in https://gitlab.com/gitlab-org/gitlab-ce/issues/53310 - # when allowing multi-line suggestions. - def from_line - position.new_line - end - alias_method :to_line, :from_line - - def from_original_line - original_position.new_line - end - alias_method :to_original_line, :from_original_line - # `from_line_index` and `to_line_index` represents diff/blob line numbers in # index-like way (N-1). def from_line_index from_line - 1 end - alias_method :to_line_index, :from_line_index - def appliable? - return false unless note.supports_suggestion? + def to_line_index + to_line - 1 + end + def appliable?(cached: true) !applied? && noteable.opened? && + !outdated?(cached: cached) && + note.supports_suggestion? && different_content? && note.active? end + # Overwrites outdated column + def outdated?(cached: true) + return super() if cached + return true unless diff_file + + from_content != fetch_from_content + end + + def target_line + position.new_line + end + private def different_content? diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index 066e30cd3bb..d3d5883e46b 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -57,7 +57,7 @@ class DiffFileEntity < DiffFileBaseEntity diff_file.diff_lines_for_serializer end - expose :is_fully_expanded, if: -> (diff_file, _) { Feature.enabled?(:expand_diff_full_file, default_enabled: true) && diff_file.text? } do |diff_file| + expose :is_fully_expanded, if: -> (diff_file, _) { diff_file.text? } do |diff_file| diff_file.fully_expanded? end diff --git a/app/services/concerns/suggestible.rb b/app/services/concerns/suggestible.rb new file mode 100644 index 00000000000..0b9822b1909 --- /dev/null +++ b/app/services/concerns/suggestible.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Suggestible + extend ActiveSupport::Concern + + # This translates into limiting suggestion changes to `suggestion:-100+100`. + MAX_LINES_CONTEXT = 100.freeze + + def fetch_from_content + diff_file.new_blob_lines_between(from_line, to_line).join + end + + def from_line + real_above = [lines_above, MAX_LINES_CONTEXT].min + [target_line - real_above, 1].max + end + + def to_line + real_below = [lines_below, MAX_LINES_CONTEXT].min + target_line + real_below + end + + def diff_file + raise NotImplementedError + end + + def target_line + raise NotImplementedError + end +end diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb index 6713b6617ae..a3cc6014fd3 100644 --- a/app/services/concerns/users/participable_service.rb +++ b/app/services/concerns/users/participable_service.rb @@ -28,19 +28,35 @@ module Users end def groups - current_user.authorized_groups.sort_by(&:path).map do |group| - group_as_hash(group) + group_counts = GroupMember + .in_groups(current_user.authorized_groups) + .non_request + .count_users_by_group_id + + current_user.authorized_groups.with_route.sort_by(&:path).map do |group| + group_as_hash(group, group_counts) end end private def user_as_hash(user) - { type: user.class.name, username: user.username, name: user.name, avatar_url: user.avatar_url } + { + type: user.class.name, + username: user.username, + name: user.name, + avatar_url: user.avatar_url + } end - def group_as_hash(group) - { type: group.class.name, username: group.full_path, name: group.full_name, avatar_url: group.avatar_url, count: group.users.count } + def group_as_hash(group, group_counts) + { + type: group.class.name, + username: group.full_path, + name: group.full_name, + avatar_url: group.avatar_url, + count: group_counts.fetch(group.id, 0) + } end end end diff --git a/app/services/merge_requests/migrate_external_diffs_service.rb b/app/services/merge_requests/migrate_external_diffs_service.rb new file mode 100644 index 00000000000..16050244637 --- /dev/null +++ b/app/services/merge_requests/migrate_external_diffs_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module MergeRequests + class MigrateExternalDiffsService < ::BaseService + MAX_JOBS = 1000.freeze + + attr_reader :diff + + def self.enqueue! + ids = MergeRequestDiff.ids_for_external_storage_migration(limit: MAX_JOBS) + + MigrateExternalDiffsWorker.bulk_perform_async(ids.map { |id| [id] }) + end + + def initialize(merge_request_diff) + @diff = merge_request_diff + end + + def execute + diff.migrate_files_to_external_storage! + end + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index f712b8863cd..f5dc5a0256d 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -20,6 +20,7 @@ module MergeRequests close_upon_missing_source_branch_ref post_merge_manually_merged reload_merge_requests + outdate_suggestions reset_merge_when_pipeline_succeeds mark_pending_todos_done cache_merge_requests_closing_issues @@ -125,6 +126,14 @@ module MergeRequests merge_request.source_branch == @push.branch_name end + def outdate_suggestions + outdate_service = Suggestions::OutdateService.new + + merge_requests_for_source_branch.each do |merge_request| + outdate_service.execute(merge_request) + end + end + def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index f778c5aa5f5..8ba50e22b09 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -7,7 +7,7 @@ module Suggestions end def execute(suggestion) - unless suggestion.appliable? + unless suggestion.appliable?(cached: false) return error('Suggestion is not appliable') end @@ -15,7 +15,7 @@ module Suggestions return error('The file has been changed') end - diff_file = suggestion.note.latest_diff_file + diff_file = suggestion.diff_file unless diff_file return error('The file was not found') diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb index c7ac2452c53..1d3338c1b45 100644 --- a/app/services/suggestions/create_service.rb +++ b/app/services/suggestions/create_service.rb @@ -9,52 +9,24 @@ module Suggestions def execute return unless @note.supports_suggestion? - diff_file = @note.latest_diff_file - - return unless diff_file - - suggestions = Banzai::SuggestionsParser.parse(@note.note) - - # For single line suggestion we're only looking forward to - # change the line receiving the comment. Though, in - # https://gitlab.com/gitlab-org/gitlab-ce/issues/53310 - # we'll introduce a ```suggestion:L<x>-<y>, so this will - # slightly change. - comment_line = @note.position.new_line + suggestions = Gitlab::Diff::SuggestionsParser.parse(@note.note, + project: @note.project, + position: @note.position) rows = suggestions.map.with_index do |suggestion, index| - from_content = changing_lines(diff_file, comment_line, comment_line) + creation_params = + suggestion.to_hash.slice(:from_content, + :to_content, + :lines_above, + :lines_below) - # The parsed suggestion doesn't have information about the correct - # ending characters (we may have a line break, or not), so we take - # this information from the last line being changed (last - # characters). - endline_chars = line_break_chars(from_content.lines.last) - to_content = "#{suggestion}#{endline_chars}" - - { - note_id: @note.id, - from_content: from_content, - to_content: to_content, - relative_order: index - } + creation_params.merge!(note_id: @note.id, relative_order: index) end rows.in_groups_of(100, false) do |rows| Gitlab::Database.bulk_insert('suggestions', rows) end end - - private - - def changing_lines(diff_file, from_line, to_line) - diff_file.new_blob_lines_between(from_line, to_line).join - end - - def line_break_chars(line) - match = /\r\n|\r|\n/.match(line) - match[0] if match - end end end diff --git a/app/services/suggestions/outdate_service.rb b/app/services/suggestions/outdate_service.rb new file mode 100644 index 00000000000..a33aac9f6b5 --- /dev/null +++ b/app/services/suggestions/outdate_service.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Suggestions + class OutdateService + def execute(merge_request) + # rubocop: disable CodeReuse/ActiveRecord + suggestions = merge_request.suggestions.active.includes(:note) + + suggestions.find_in_batches(batch_size: 100) do |group| + outdatable_suggestion_ids = group.select do |suggestion| + suggestion.outdated?(cached: false) + end.map(&:id) + + Suggestion.where(id: outdatable_suggestion_ids).update_all(outdated: true) + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index eefe86eb6b4..b950e53639a 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -45,6 +45,8 @@ %span = _('Contribution Analytics') + = render_if_exists 'layouts/nav/group_insights_link' + = render_if_exists "layouts/nav/ee/epic_link", group: @group - if group_sidebar_link?(:issues) diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 7b492efeb09..6b33189d1cf 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -41,6 +41,8 @@ = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do %span= _('Cycle Analytics') + = render_if_exists 'layouts/nav/project_insights_link' + - if project_nav_tab? :files = nav_link(controller: sidebar_repository_paths) do = link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 09295940529..6a7cb1499c5 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -4,7 +4,7 @@ = render "projects/jobs/header" - add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project)) -- add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project)) +- add_to_breadcrumbs("##{@build.id}", project_job_path(@project, @build)) .tree-holder .nav-block diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 2e62039b90a..5b25a67bc87 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -21,6 +21,8 @@ - if @scope == 'projects' .term = render 'shared/projects/list', projects: @search_objects, pipeline_status: false + - elsif %w[blobs wiki_blobs].include?(@scope) + = render partial: 'search/results/blob', collection: @search_objects, locals: { projects: blob_projects(@search_objects) } - else = render partial: "search/results/#{@scope.singularize}", collection: @search_objects diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index 2a602095845..bdad07f36d1 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,4 +1,4 @@ -- project = find_project_for_result_blob(blob) +- project = find_project_for_result_blob(projects, blob) - return unless project - blob = parse_search_result(blob) diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index 389e4cc75b9..b351ecd4edf 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,4 +1,4 @@ -- project = find_project_for_result_blob(wiki_blob) +- project = find_project_for_result_blob(projects, wiki_blob) - wiki_blob = parse_search_result(wiki_blob) - wiki_blob_link = project_wiki_path(project, wiki_blob.basename) diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 6ebd756d3da..3e8c2a1209a 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -21,6 +21,7 @@ - cronjob:trending_projects - cronjob:issue_due_scheduler - cronjob:prune_web_hook_logs +- cronjob:schedule_migrate_external_diffs - gcp_cluster:cluster_install_app - gcp_cluster:cluster_patch_app @@ -119,6 +120,7 @@ - invalid_gpg_signature_update - irker - merge +- migrate_external_diffs - namespaceless_project_destroy - new_issue - new_merge_request diff --git a/app/workers/migrate_external_diffs_worker.rb b/app/workers/migrate_external_diffs_worker.rb new file mode 100644 index 00000000000..debac97af2c --- /dev/null +++ b/app/workers/migrate_external_diffs_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: false + +class MigrateExternalDiffsWorker + include ApplicationWorker + + def perform(merge_request_diff_id) + diff = MergeRequestDiff.find_by_id(merge_request_diff_id) + return unless diff + + MergeRequests::MigrateExternalDiffsService.new(diff).execute + end +end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index a40c865a5e5..396f44396a3 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -4,7 +4,7 @@ class PostReceive include ApplicationWorker def perform(gl_repository, identifier, changes, push_options = []) - project, is_wiki = Gitlab::GlRepository.parse(gl_repository) + project, repo_type = Gitlab::GlRepository.parse(gl_repository) if project.nil? log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"") @@ -17,7 +17,7 @@ class PostReceive Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS'] post_received = Gitlab::GitPostReceive.new(project, identifier, changes, push_options) - if is_wiki + if repo_type.wiki? process_wiki_changes(post_received) else process_project_changes(post_received) diff --git a/app/workers/schedule_migrate_external_diffs_worker.rb b/app/workers/schedule_migrate_external_diffs_worker.rb new file mode 100644 index 00000000000..70910f7ca04 --- /dev/null +++ b/app/workers/schedule_migrate_external_diffs_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: false + +class ScheduleMigrateExternalDiffsWorker + include ApplicationWorker + include CronjobQueue + include Gitlab::ExclusiveLeaseHelpers + + def perform + in_lock(self.class.name.underscore, ttl: 2.hours, retries: 0) do + MergeRequests::MigrateExternalDiffsService.enqueue! + end + rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + end +end diff --git a/changelogs/unreleased/54670-external-diffs-when-outdated.yml b/changelogs/unreleased/54670-external-diffs-when-outdated.yml new file mode 100644 index 00000000000..2a0b9e75cb4 --- /dev/null +++ b/changelogs/unreleased/54670-external-diffs-when-outdated.yml @@ -0,0 +1,5 @@ +--- +title: 'Allow external diffs to be used conditionally' +merge_request: 25432 +author: +type: added diff --git a/changelogs/unreleased/59062-update-gitlab-markup-python-3.yml b/changelogs/unreleased/59062-update-gitlab-markup-python-3.yml new file mode 100644 index 00000000000..265a7e36841 --- /dev/null +++ b/changelogs/unreleased/59062-update-gitlab-markup-python-3.yml @@ -0,0 +1,5 @@ +--- +title: Update gitlab-markup to 1.7.0 which requies python3 +merge_request: 26246 +author: +type: changed diff --git a/changelogs/unreleased/59079-fix-jupyter-render-loop.yml b/changelogs/unreleased/59079-fix-jupyter-render-loop.yml new file mode 100644 index 00000000000..29264b33dfa --- /dev/null +++ b/changelogs/unreleased/59079-fix-jupyter-render-loop.yml @@ -0,0 +1,5 @@ +--- +title: Fix jupyter rendering bug that ended in an infinite loop +merge_request: 26656 +author: ROSPARS Benoit +type: fixed diff --git a/changelogs/unreleased/59273-update-fugit.yml b/changelogs/unreleased/59273-update-fugit.yml new file mode 100644 index 00000000000..3a1c64d87ef --- /dev/null +++ b/changelogs/unreleased/59273-update-fugit.yml @@ -0,0 +1,5 @@ +--- +title: Update fugit which fixes a potential infinite loop +merge_request: 26579 +author: +type: fixed diff --git a/changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml b/changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml new file mode 100644 index 00000000000..440b24a548c --- /dev/null +++ b/changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml @@ -0,0 +1,5 @@ +--- +title: Add select by title to milestones API +merge_request: 26573 +author: +type: added diff --git a/changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml b/changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml new file mode 100644 index 00000000000..da65c3bc870 --- /dev/null +++ b/changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml @@ -0,0 +1,5 @@ +--- +title: Fixes job link in artifacts page breadcrumb +merge_request: 26592 +author: +type: fixed diff --git a/changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml b/changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml new file mode 100644 index 00000000000..8f0ce5d57c5 --- /dev/null +++ b/changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml @@ -0,0 +1,5 @@ +--- +title: Fix Auto DevOps missing domain error handling +merge_request: 26627 +author: +type: fixed diff --git a/changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml b/changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml new file mode 100644 index 00000000000..ae097e859d9 --- /dev/null +++ b/changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml @@ -0,0 +1,5 @@ +--- +title: Fix some N+1s in loading routes and counting members for groups in @-autocomplete +merge_request: 26491 +author: +type: performance diff --git a/changelogs/unreleased/issue_58547.yml b/changelogs/unreleased/issue_58547.yml new file mode 100644 index 00000000000..553c752e72d --- /dev/null +++ b/changelogs/unreleased/issue_58547.yml @@ -0,0 +1,5 @@ +--- +title: Add API access check to Graphql +merge_request: 26570 +author: +type: other diff --git a/changelogs/unreleased/knative-update.yml b/changelogs/unreleased/knative-update.yml new file mode 100644 index 00000000000..e84940ae7e0 --- /dev/null +++ b/changelogs/unreleased/knative-update.yml @@ -0,0 +1,5 @@ +--- +title: Knative version bump 0.2.2 -> 0.3.0 +merge_request: 26459 +author: Chris Baumbauer +type: changed diff --git a/changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml b/changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml new file mode 100644 index 00000000000..01bd7ede270 --- /dev/null +++ b/changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml @@ -0,0 +1,5 @@ +--- +title: Implements the creation strategy for multi-line suggestions +merge_request: 26057 +author: +type: changed diff --git a/changelogs/unreleased/sh-add-gitaly-duration-logs.yml b/changelogs/unreleased/sh-add-gitaly-duration-logs.yml new file mode 100644 index 00000000000..eea50384278 --- /dev/null +++ b/changelogs/unreleased/sh-add-gitaly-duration-logs.yml @@ -0,0 +1,5 @@ +--- +title: Log Gitaly RPC duration to api_json.log and production_json.log +merge_request: 26652 +author: +type: other diff --git a/changelogs/unreleased/sh-optimize-projects-api.yml b/changelogs/unreleased/sh-optimize-projects-api.yml new file mode 100644 index 00000000000..2f2459be77f --- /dev/null +++ b/changelogs/unreleased/sh-optimize-projects-api.yml @@ -0,0 +1,5 @@ +--- +title: Optimize /api/v4/projects endpoint for visibility level +merge_request: 26481 +author: +type: performance diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index eba7d2b9fb7..8d9b6624995 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -301,6 +301,10 @@ production: &base pages_domain_verification_cron_worker: cron: "*/15 * * * *" + # Periodically migrate diffs from the database to external storage + schedule_migrate_external_diffs_worker: + cron: "15 * * * *" + registry: # enabled: true # host: registry.example.com @@ -787,6 +791,10 @@ test: enabled: true external_diffs: enabled: false + # Diffs may be `always` external (the default), or they can be made external + # after they have become `outdated` (i.e., the MR is closed or a new version + # has been pushed). + # when: always # The location where external diffs are stored (default: shared/external-diffs). # storage_path: shared/external-diffs object_store: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 99bdf5a95c2..01ffcade931 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -238,6 +238,7 @@ Settings.pages.admin['certificate'] ||= '' # Settings['external_diffs'] ||= Settingslogic.new({}) Settings.external_diffs['enabled'] = false if Settings.external_diffs['enabled'].nil? +Settings.external_diffs['when'] = 'always' if Settings.external_diffs['when'].nil? Settings.external_diffs['storage_path'] = Settings.absolute(Settings.external_diffs['storage_path'] || File.join(Settings.shared['path'], 'external-diffs')) Settings.external_diffs['object_store'] = ObjectStoreSettings.parse(Settings.external_diffs['object_store']) @@ -344,6 +345,10 @@ Settings.cron_jobs['prune_web_hook_logs_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['prune_web_hook_logs_worker']['cron'] ||= '0 */1 * * *' Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLogsWorker' +Settings.cron_jobs['schedule_migrate_external_diffs_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *' +Settings.cron_jobs['schedule_migrate_external_diffs_worker']['job_class'] = 'ScheduleMigrateExternalDiffsWorker' + # # Sidekiq # diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 164954d1293..5e790a9eccb 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -28,7 +28,12 @@ unless Sidekiq.server? } gitaly_calls = Gitlab::GitalyClient.get_request_count - payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0 + + if gitaly_calls > 0 + payload[:gitaly_calls] = gitaly_calls + payload[:gitaly_duration] = Gitlab::GitalyClient.query_time_ms + end + payload[:response] = event.payload[:response] if event.payload[:response] payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index cef123b86ae..2dc0da00919 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -89,3 +89,4 @@ - [project_daily_statistics, 1] - [import_issues_csv, 2] - [chat_notification, 2] + - [migrate_external_diffs, 1] diff --git a/db/fixtures/development/25_api_personal_access_token.rb b/db/fixtures/development/25_api_personal_access_token.rb new file mode 100644 index 00000000000..a2e6c674c1f --- /dev/null +++ b/db/fixtures/development/25_api_personal_access_token.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require './spec/support/sidekiq' + +# Create an api access token for root user with the value: ypCa3Dzb23o5nvsixwPA +Gitlab::Seeder.quiet do + PersonalAccessToken.create!( + user_id: User.find_by(username: 'root').id, + name: "seeded-api-token", + scopes: ["api"], + token_digest: "/O0jfLERYT/L5gG8nfByQxqTj43TeLlRzOtJGTzRsbQ=" + ) + + print '.' +end diff --git a/db/migrate/20190222051615_add_indexes_for_merge_request_diffs_query.rb b/db/migrate/20190222051615_add_indexes_for_merge_request_diffs_query.rb new file mode 100644 index 00000000000..0048268ca6f --- /dev/null +++ b/db/migrate/20190222051615_add_indexes_for_merge_request_diffs_query.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class AddIndexesForMergeRequestDiffsQuery < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_SPECS = [ + [ + :merge_request_metrics, + :latest_closed_at, + { where: 'latest_closed_at IS NOT NULL' } + ], + [ + :merge_request_metrics, + [:merge_request_id, :merged_at], + { where: 'merged_at IS NOT NULL' } + ], + [ + :merge_request_diffs, + [:merge_request_id, :id], + { + name: 'index_merge_request_diffs_on_merge_request_id_and_id_partial', + where: 'NOT stored_externally OR stored_externally IS NULL' + } + ] + ].freeze + + disable_ddl_transaction! + + def up + INDEX_SPECS.each do |spec| + add_concurrent_index(*spec) + end + end + + def down + INDEX_SPECS.reverse.each do |spec| + remove_concurrent_index(*spec) + end + end +end diff --git a/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb b/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb new file mode 100644 index 00000000000..856dfc89fa3 --- /dev/null +++ b/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddMultiLineAttributesToSuggestion < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :suggestions, :lines_above, :integer, default: 0, allow_null: false + add_column_with_default :suggestions, :lines_below, :integer, default: 0, allow_null: false + add_column_with_default :suggestions, :outdated, :boolean, default: false, allow_null: false + end + + def down + remove_columns :suggestions, :outdated, :lines_above, :lines_below + end +end diff --git a/db/schema.rb b/db/schema.rb index 0c997f3b8a2..7cc09e56285 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1256,6 +1256,7 @@ ActiveRecord::Schema.define(version: 20190322132835) do t.integer "external_diff_store" t.boolean "stored_externally" t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree + t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id_partial", where: "((NOT stored_externally) OR (stored_externally IS NULL))", using: :btree end create_table "merge_request_metrics", force: :cascade do |t| @@ -1271,7 +1272,9 @@ ActiveRecord::Schema.define(version: 20190322132835) do t.integer "latest_closed_by_id" t.datetime_with_timezone "latest_closed_at" t.index ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree + t.index ["latest_closed_at"], name: "index_merge_request_metrics_on_latest_closed_at", where: "(latest_closed_at IS NOT NULL)", using: :btree t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id", using: :btree + t.index ["merge_request_id", "merged_at"], name: "index_merge_request_metrics_on_merge_request_id_and_merged_at", where: "(merged_at IS NOT NULL)", using: :btree t.index ["merge_request_id"], name: "index_merge_request_metrics", using: :btree t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id", using: :btree t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree @@ -2039,6 +2042,9 @@ ActiveRecord::Schema.define(version: 20190322132835) do t.string "commit_id" t.text "from_content", null: false t.text "to_content", null: false + t.integer "lines_above", default: 0, null: false + t.integer "lines_below", default: 0, null: false + t.boolean "outdated", default: false, null: false t.index ["note_id", "relative_order"], name: "index_suggestions_on_note_id_and_relative_order", unique: true, using: :btree end diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index b1aaa3bca13..f406163aea0 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -71,9 +71,8 @@ bug](https://bugzilla.redhat.com/show_bug.cgi?id=1552203) that may be fixed in [more recent kernels with this commit](https://github.com/torvalds/linux/commit/95da1b3a5aded124dd1bda1e3cdb876184813140). -Users encountering a similar issue may be advised to disable the NFS server -delegation feature, which is an optimization to reduce the number of network -round-trips needed to read or write files. To disable NFS server delegations +GitLab recommends all NFS users disable the NFS server +delegation feature. To disable NFS server delegations on an Linux NFS server, do the following: 1. On the NFS server, run: diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 36dee75bd44..3d40cda491a 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -23,16 +23,19 @@ requests from the API are logged to a separate file in `api_json.log`. Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example: ```json -{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":[{"key":"param_key","value":"param_value"}],"remote_ip":"18.245.0.1","user_id":1,"username":"admin","gitaly_calls":76,"queue_duration": 112.47} +{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":[{"key":"param_key","value":"param_value"}],"remote_ip":"18.245.0.1","user_id":1,"username":"admin","gitaly_calls":76,"gitaly_duration":7.41,"queue_duration": 112.47} ``` -In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data: +In this example, you can see this was a GET request for a specific +issue. Notice each line also contains performance data. All times are in +milliseconds: -1. `duration`: total time in milliseconds taken to retrieve the request -1. `queue_duration`: total time in milliseconds that the request was queued inside GitLab Workhorse +1. `duration`: total time taken to retrieve the request +1. `queue_duration`: total time that the request was queued inside GitLab Workhorse 1. `view`: total time taken inside the Rails views 1. `db`: total time to retrieve data from the database 1. `gitaly_calls`: total number of calls made to Gitaly +1. `gitaly_duration`: total time taken by Gitaly calls User clone/fetch activity using http transport appears in this log as `action: git_upload_pack`. @@ -85,7 +88,7 @@ Introduced in GitLab 10.0, this file lives in It helps you see requests made directly to the API. For example: ```json -{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30} +{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36} ``` This entry above shows an access to an internal endpoint to check whether an diff --git a/doc/administration/merge_request_diffs.md b/doc/administration/merge_request_diffs.md index c34a9519ace..5e9ba4f640f 100644 --- a/doc/administration/merge_request_diffs.md +++ b/doc/administration/merge_request_diffs.md @@ -145,3 +145,47 @@ The connection settings match those provided by [Fog](https://github.com/fog), a ``` 1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect. + +### Alternative in-database storage + +Enabling external diffs may reduce the performance of merge requests, as they +must be retrieved in a separate operation to other data. A compromise may be +reached by only storing outdated diffs externally, while keeping current diffs +in the database. + +To enable this feature, perform the following steps: + +**In Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['external_diffs_when'] = 'outdated' + ``` + +1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. + +**In installations from source:** + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following + lines: + + ```yaml + external_diffs: + enabled: true + when: outdated + ``` + +1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect. + +With this feature enabled, diffs will initially stored in the database, rather +than externally. They will be moved to external storage once any of these +conditions become true: + +- A newer version of the merge request diff exists +- The merge request was merged more than seven days ago +- The merge request was closed more than seven day ago + +These rules strike a balance between space and performance by only storing +frequently-accessed diffs in the database. Diffs that are less likely to be +accessed are moved to external storage instead. diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index 1c2f56581eb..260eb09cc38 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -12,6 +12,7 @@ GET /groups/:id/milestones?iids[]=42 GET /groups/:id/milestones?iids[]=42&iids[]=43 GET /groups/:id/milestones?state=active GET /groups/:id/milestones?state=closed +GET /groups/:id/milestones?title=1.0 GET /groups/:id/milestones?search=version ``` @@ -22,6 +23,7 @@ Parameters: | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` | | `state` | string | optional | Return only `active` or `closed` milestones | +| `title` | string | optional | Return only the milestones having the given `title` | | `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 897184d51af..3b76c19dc07 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -10,6 +10,7 @@ GET /projects/:id/milestones?iids[]=42 GET /projects/:id/milestones?iids[]=42&iids[]=43 GET /projects/:id/milestones?state=active GET /projects/:id/milestones?state=closed +GET /projects/:id/milestones?title=1.0 GET /projects/:id/milestones?search=version ``` @@ -20,6 +21,7 @@ Parameters: | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` | | `state` | string | optional | Return only `active` or `closed` milestones | +| `title` | string | optional | Return only the milestones having the given `title` | | `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash diff --git a/doc/api/suggestions.md b/doc/api/suggestions.md index e88d536282a..188989bc94e 100644 --- a/doc/api/suggestions.md +++ b/doc/api/suggestions.md @@ -24,8 +24,6 @@ Example response: ```json { "id": 36, - "from_original_line": 10, - "to_original_line": 10, "from_line": 10, "to_line": 10, "appliable": false, diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md index 0eedef5e14f..45104a1f91d 100644 --- a/doc/development/contributing/style_guides.md +++ b/doc/development/contributing/style_guides.md @@ -22,6 +22,7 @@ text should be _sorry, we could not create your account because:_ 1. Code should be written in [US English][us-english] 1. [Go](../go_guide/index.md) +1. [Python](../python_guide/index.md) This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop) and [Hound CI](https://houndci.com). diff --git a/doc/development/python_guide/index.md b/doc/development/python_guide/index.md new file mode 100644 index 00000000000..6025dc9ebf2 --- /dev/null +++ b/doc/development/python_guide/index.md @@ -0,0 +1,79 @@ +# Python Development Guidelines + +GitLab requires Python as a dependency for [reStructuredText](http://docutils.sourceforge.net/rst.html) +markup rendering. + +As of GitLab 11.10, we require Python 3. + +## Installation + +There are several ways of installing python on your system. To be able to use the same version we use in production, +we suggest you use [pyenv](https://github.com/pyenv/pyenv). It works and behave similar to its counterpart in the +ruby world: [rbenv](https://github.com/rbenv/rbenv). + +### macOS + +To install `pyenv` on macOS, you can use [Homebrew](https://brew.sh/) with: + +```bash +brew install pyenv +``` + +### Linux + +To install `pyenv` on Linux, you can run the command below: + +```bash +curl https://pyenv.run | bash +``` + +Alternatively, you may find `pypenv` available as a system package via your distro package manager. + +You can read more about it in: <https://github.com/pyenv/pyenv-installer#prerequisites>. + +### Shell integration + +Pyenv installation will add required changes to Bash. If you use a different shell, +check for any additional steps required for it. + +For Fish, you can install a plugin for [Fisherman](https://github.com/fisherman/fisherman): + +```bash +fisher add fisherman/pyenv +``` + +Or for [Oh My Fish](https://github.com/oh-my-fish/oh-my-fish): + +```bash +omf install pyenv +``` + +## Dependency management + +While GitLab doesn't directly contain any Python scripts, because we depend on Python to render +[reStructuredText](http://docutils.sourceforge.net/rst.html) markup, we need to keep track on dependencies +on the main project level, so we can run that on our development machines. + +Recently, an equivalent to the `Gemfile` and the [Bundler](https://bundler.io/) project has been introduced to Python: +`Pipfile` and [Pipenv](https://pipenv.readthedocs.io/en/latest/). + +You will now find a `Pipfile` with the dependencies in the root folder. To install them, run: + +```bash +pipenv install +``` + +Running this command will install both the required Python version as well as required pip dependencies. + +## Use instructions + +To run any python code under the Pipenv environment, you need to first start a `virtualenv` based on the dependencies +of the application. With Pipenv, this is a simple as running: + +```bash +pipenv shell +``` + +After running that command, you can run GitLab on the same shell and it will be using the Python and dependencies +installed from the `pipenv install` command. + diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index cfe0e6f70fc..7262c04d746 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -1,5 +1,20 @@ # Testing best practices +## Test Design + +Testing at GitLab is a first class citizen, not an afterthought. It's important we consider the design of our tests +as we do the design of our features. + +When implementing a feature, we think about developing the right capabilities the right way, which helps us +narrow our scope to a manageable level. When implementing tests for a feature, we must think about developing +the right tests, but then cover _all_ the important ways the test may fail, which can quickly widen our scope to +a level that is difficult to manage. + +Test heuristics can help solve this problem. They concisely address many of the common ways bugs +manifest themselves within our code. When designing our tests, take time to review known test heuristics to inform +our test design. We can find some helpful heuristics documented in the Handbook in the +[Test Design](https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/test-design/) section. + ## Test speed GitLab has a massive test suite that, without [parallelization], can take hours diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 67e4cfeda0e..ecad9ba48a3 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -33,7 +33,7 @@ changes should be tested. ## [Testing best practices](best_practices.md) -Everything you should know about how to write good tests: RSpec, FactoryBot, +Everything you should know about how to write good tests: Test Design, RSpec, FactoryBot, system tests, parameterized tests etc. --- diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index d02aa24cd85..0000e03f1d7 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -1,11 +1,11 @@ -# Installing GitLab on Amazon Web Services (AWS) - -To install GitLab on AWS, you can use the Amazon Machine Images (AMIs) that GitLab -provides with [each release](https://about.gitlab.com/releases/). +# Installing GitLab HA on Amazon Web Services (AWS) This page offers a walkthrough of a common HA (Highly Available) configuration for GitLab on AWS. You should customize it to accommodate your needs. +NOTE: **Note** +For organizations with 300 users or less, the recommended AWS installation method is to launch an EC2 single box [Omnibus Installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data. + ## Introduction GitLab on AWS can leverage many of the services that are already diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 11f24b4b701..b9e3e6aea69 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -652,7 +652,7 @@ repo or by specifying a project variable: ### Custom Helm chart per environment **[PREMIUM]** -You can specify the use of a custom Helm chart per environment by scoping the environment variable +You can specify the use of a custom Helm chart per environment by scoping the environment variable to the desired environment. See [Limiting environment scopes of variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-variables-premium). ### Customizing `.gitlab-ci.yml` @@ -1022,10 +1022,9 @@ planned for a subsequent release. buildpack](#custom-buildpacks). - Auto Test may fail because of a mismatch between testing frameworks. In this case, you may need to customize your `.gitlab-ci.yml` with your test commands. -- Auto Deploy may fail if it is unable to create a Kubernetes namespace and - service account for your project. See the - [troubleshooting failed deployments](../../user/project/clusters/index.md#troubleshooting-failed-deployment-jobs) - section to debug why these resources were not created. +- Auto Deploy will fail if GitLab can not create a Kubernetes namespace and + service account for your project. For help debugging this issue, see + [Troubleshooting failed deployment jobs](../../user/project/clusters/index.md#troubleshooting-failed-deployment-jobs). ### Disable the banner instance wide diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index f6bb342de43..984881ef26c 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -28,7 +28,7 @@ deployments. | [Helm Tiller](https://docs.helm.sh) | 11.6+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a | | [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress) | 11.6+ | 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. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) | | [Cert-Manager](https://docs.cert-manager.io/en/latest/) | 11.6+ | Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) | -| [GitLab Runner](https://docs.gitlab.com/runner/) | 11.10+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](../../../ci/README.md), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) | +| [GitLab Runner](https://docs.gitlab.com/runner/) | 11.10+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](../../../ci/README.md), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](../../project/clusters/index.md#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) | NOTE: **Note:** Some [cluster diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index b74bd81d467..8b3ae19b544 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -18,9 +18,10 @@ the second factor of authentication. Once enabled, in addition to supplying your password to login, you'll be prompted to activate your U2F device (usually by pressing a button on it), and it will perform secure authentication on your behalf. -The U2F workflow is only [supported by](https://caniuse.com/#search=U2F) Google Chrome, Opera and Firefox at this point, so we _strongly_ recommend -that you set up both methods of two-factor authentication, so you can still access your account -from other browsers. +The U2F workflow is [supported by](https://caniuse.com/#search=U2F) Google Chrome, Opera, and Firefox. + +We recommend that you set up 2FA with both a [one-time password authenticator](#enable-2fa-via-one-time-password-authenticator) and a [U2F device](#enable-2fa-via-u2f-device), so you can still access your account +if you lose your U2F device. ## Enabling 2FA diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 7fde27b3d22..3a9a3b4a423 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -389,14 +389,20 @@ to obtain the endpoint. You can use either In order to publish your web application, you first need to find the endpoint which will be either an IP address or a hostname associated with your load balancer. -### Let GitLab fetch the external endpoint +### Automatically determining the external endpoint > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6. -If you [installed Ingress or Knative](#installing-applications), -you should see the Ingress Endpoint on this same page within a few minutes. -If you don't see this, GitLab might not be able to determine the external endpoint of -your ingress application in which case you should manually determine it. +After you install [Ingress or Knative](#installing-applications), Gitlab attempts to determine the external endpoint +and it should be available within a few minutes. If the endpoint doesn't appear +and your cluster runs on Google Kubernetes Engine: + +1. Check your [Kubernetes cluster on Google Kubernetes Engine](https://console.cloud.google.com/kubernetes) to ensure there are no errors on its nodes. +1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas) on Google Kubernetes Engine. For more information, see [Resource Quotas](https://cloud.google.com/compute/quotas). +1. Check [Google Cloud's Status](https://status.cloud.google.com/) to ensure they are not having any disruptions. + +If GitLab is still unable to determine the endpoint of your Ingress or Knative application, you can +manually determine it by following the steps below. ### Manually determining the external endpoint @@ -542,25 +548,23 @@ service account of the cluster integration. ### Troubleshooting failed deployment jobs GitLab will create a namespace and service account specifically for your -deployment jobs. These resources are created just before the deployment -job starts. Sometimes there may be errors that cause their creation to fail. +deployment jobs, immediately before the jobs starts. -In such instances, your job will fail with the message: +However, sometimes GitLab can not create them. In such instances, your job will fail with the message: -```The job failed to complete prerequisite tasks``` +```text +The job failed to complete prerequisite tasks +``` -You will need to check the [logs](../../../administration/logs.md) to debug -why the namespace and service account creation failed. +To find the cause of this error when creating a namespace and service account, check the [logs](../../../administration/logs.md#sidekiqlog). -A common reason for failure is that the token you gave GitLab did not have -[`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) -privileges as GitLab expects. +Common reasons for failure include: -Another common problem is caused by a missing `KUBECONFIG` or `KUBE_TOKEN`. -To be passed to your job, it must have a matching -[`environment:name`](../../../ci/environments.md#defining-environments). If -your job has no `environment:name` set, it will not be passed the Kubernetes -credentials. +- The token you gave GitLab did not have [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) + privileges required by GitLab. +- Missing `KUBECONFIG` or `KUBE_TOKEN` variables. To be passed to your job, they must have a matching + [`environment:name`](../../../ci/environments.md#defining-environments). If your job has no + `environment:name` set, it will not be passed the Kubernetes credentials. ## Monitoring your Kubernetes cluster **[ULTIMATE]** diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index d324e0de0cc..8e1603f9ec9 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -48,7 +48,7 @@ Navigate to the webhooks page by going to your project's ## Use-cases - You can set up a webhook in GitLab to send a notification to - [Slack](https://api.slack.com/incoming-webhooks) every time a build fails, for example + [Slack](https://api.slack.com/incoming-webhooks) every time a job fails. - You can [integrate with Twilio to be notified via SMS](https://www.datadoghq.com/blog/send-alerts-sms-customizable-webhooks-twilio/) every time an issue is created for a specific project or group within GitLab - You can use them to [automatically assign labels to merge requests](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/). @@ -1004,7 +1004,7 @@ X-Gitlab-Event: Pipeline Hook "email": "user@gitlab.com" } }, - "builds":[ + "jobs":[ { "id": 380, "stage": "deploy", @@ -1129,34 +1129,34 @@ X-Gitlab-Event: Pipeline Hook } ``` -### Build events +### Job events -Triggered on status change of a Build. +Triggered on status change of a job. **Request Header**: ``` -X-Gitlab-Event: Build Hook +X-Gitlab-Event: Job Hook ``` **Request Body**: ```json { - "object_kind": "build", + "object_kind": "job", "ref": "gitlab-script-trigger", "tag": false, "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "build_id": 1977, - "build_name": "test", - "build_stage": "test", - "build_status": "created", - "build_started_at": null, - "build_finished_at": null, - "build_duration": null, - "build_allow_failure": false, - "build_failure_reason": "script_failure", + "job_id": 1977, + "job_name": "test", + "job_stage": "test", + "job_status": "created", + "job_started_at": null, + "job_finished_at": null, + "job_duration": null, + "job_allow_failure": false, + "job_failure_reason": "script_failure", "project_id": 380, "project_name": "gitlab-org/gitlab-test", "user": { diff --git a/jest.config.js b/jest.config.js index 1f6e04390ae..cd0d311779d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,4 +37,5 @@ module.exports = { }, transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui)/)'], timers: 'fake', + testEnvironment: '<rootDir>/spec/frontend/environment.js', }; diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 6fd267ff2ed..0871ea8d21e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1558,8 +1558,6 @@ module API class Suggestion < Grape::Entity expose :id - expose :from_original_line - expose :to_original_line expose :from_line expose :to_line expose :appliable?, as: :appliable diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index b8bd180bdc1..8a21d44b4bf 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -302,6 +302,12 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + def filter_by_title(items, title) + items.where(title: title) + end + # rubocop: enable CodeReuse/ActiveRecord + def filter_by_search(items, text) items.search(text) end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index fe78049af87..3fd824877ae 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -5,9 +5,11 @@ module API module InternalHelpers attr_reader :redirected_path - def wiki? - set_project unless defined?(@wiki) # rubocop:disable Gitlab/ModuleWithInstanceVariables - @wiki # rubocop:disable Gitlab/ModuleWithInstanceVariables + delegate :wiki?, to: :repo_type + + def repo_type + set_project unless defined?(@repo_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables + @repo_type # rubocop:disable Gitlab/ModuleWithInstanceVariables end def project @@ -67,10 +69,10 @@ module API # rubocop:disable Gitlab/ModuleWithInstanceVariables def set_project if params[:gl_repository] - @project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository]) + @project, @repo_type = Gitlab::GlRepository.parse(params[:gl_repository]) @redirected_path = nil else - @project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project]) + @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(params[:project]) end end # rubocop:enable Gitlab/ModuleWithInstanceVariables @@ -78,7 +80,7 @@ module API # Project id to pass between components that don't share/don't have # access to the same filesystem mounts def gl_repository - Gitlab::GlRepository.gl_repository(project, wiki?) + repo_type.identifier_for_subject(project) end def gl_project_path @@ -92,7 +94,7 @@ module API # Return the repository depending on whether we want the wiki or the # regular repository def repository - if wiki? + if repo_type.wiki? project.wiki.repository else project.repository diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 7f4a00f1389..cb9aa849eeb 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -59,7 +59,7 @@ module API actor end - access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess + access_checker_klass = repo_type.access_checker_class access_checker = access_checker_klass.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, namespace_path: namespace_path, project_path: project_path, diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index a0ca39b69d4..62e159ab003 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -16,6 +16,7 @@ module API optional :state, type: String, values: %w[active closed all], default: 'all', desc: 'Return "active", "closed", or "all" milestones' optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :title, type: String, desc: 'The title of the milestones' optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' use :pagination end @@ -33,6 +34,7 @@ module API milestones = parent.milestones.order_id_desc milestones = Milestone.filter_by_state(milestones, params[:state]) milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? + milestones = filter_by_title(milestones, params[:title]) if params[:title] milestones = filter_by_search(milestones, params[:search]) if params[:search] present paginate(milestones), with: Entities::Milestone diff --git a/lib/banzai/suggestions_parser.rb b/lib/banzai/suggestions_parser.rb index 09f36635020..0d7f751bfc1 100644 --- a/lib/banzai/suggestions_parser.rb +++ b/lib/banzai/suggestions_parser.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# TODO: Delete when https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26107 +# exchange this parser by `Gitlab::Diff::SuggestionsParser`. module Banzai module SuggestionsParser # Returns the content of each suggestion code block. diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 7ec786b6d5d..78872b3bbe3 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -856,7 +856,7 @@ rollout 100%: function check_kube_domain() { ensure_kube_ingress_base_domain - if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ]; then + if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then echo "In order to deploy or use Review Apps," echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set" echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings" diff --git a/lib/gitlab/diff/suggestion.rb b/lib/gitlab/diff/suggestion.rb new file mode 100644 index 00000000000..027c7a31bcf --- /dev/null +++ b/lib/gitlab/diff/suggestion.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class Suggestion + include Suggestible + include Gitlab::Utils::StrongMemoize + + attr_reader :diff_file, :lines_above, :lines_below, + :target_line + + def initialize(text, line:, above:, below:, diff_file:) + @text = text + @target_line = line + @lines_above = above.to_i + @lines_below = below.to_i + @diff_file = diff_file + end + + def to_hash + { + from_content: from_content, + to_content: to_content, + lines_above: @lines_above, + lines_below: @lines_below + } + end + + def from_content + strong_memoize(:from_content) do + fetch_from_content + end + end + + def to_content + # The parsed suggestion doesn't have information about the correct + # ending characters (we may have a line break, or not), so we take + # this information from the last line being changed (last + # characters). + endline_chars = line_break_chars(from_content.lines.last) + "#{@text}#{endline_chars}" + end + + private + + def line_break_chars(line) + match = /\r\n|\r|\n/.match(line) + match[0] if match + end + end + end +end diff --git a/lib/gitlab/diff/suggestions_parser.rb b/lib/gitlab/diff/suggestions_parser.rb index 043bd9a4bcb..c8c03d5d001 100644 --- a/lib/gitlab/diff/suggestions_parser.rb +++ b/lib/gitlab/diff/suggestions_parser.rb @@ -5,6 +5,47 @@ module Gitlab class SuggestionsParser # Matches for instance "-1", "+1" or "-1+2". SUGGESTION_CONTEXT = /^(\-(?<above>\d+))?(\+(?<below>\d+))?$/.freeze + + class << self + # Returns an array of Gitlab::Diff::Suggestion which represents each + # suggestion in the given text. + # + def parse(text, position:, project:) + return [] unless position.complete? + + html = Banzai.render(text, project: nil, no_original_data: true) + doc = Nokogiri::HTML(html) + suggestion_nodes = doc.search('pre.suggestion') + + return [] if suggestion_nodes.empty? + + diff_file = position.diff_file(project.repository) + + suggestion_nodes.map do |node| + lang_param = node['data-lang-params'] + + lines_above, lines_below = nil + + if lang_param && suggestion_params = fetch_suggestion_params(lang_param) + lines_above, lines_below = + suggestion_params[:above], + suggestion_params[:below] + end + + Gitlab::Diff::Suggestion.new(node.text, + line: position.new_line, + above: lines_above.to_i, + below: lines_below.to_i, + diff_file: diff_file) + end + end + + private + + def fetch_suggestion_params(lang_param) + lang_param.match(SUGGESTION_CONTEXT) + end + end end end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index e3b9a7a1a89..49cff7517e9 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -33,12 +33,6 @@ module Gitlab MUTEX = Mutex.new - class << self - attr_accessor :query_time - end - - self.query_time = 0 - define_histogram :gitaly_controller_action_duration_seconds do docstring "Gitaly endpoint histogram by controller and action combination" base_labels Gitlab::Metrics::Transaction::BASE_LABELS.merge(gitaly_service: nil, rpc: nil) @@ -174,6 +168,18 @@ module Gitlab add_call_details(feature: "#{service}##{rpc}", duration: duration, request: request_hash, rpc: rpc) end + def self.query_time + SafeRequestStore[:gitaly_query_time] ||= 0 + end + + def self.query_time=(duration) + SafeRequestStore[:gitaly_query_time] = duration + end + + def self.query_time_ms + (self.query_time * 1000).round(2) + end + def self.current_transaction_labels Gitlab::Metrics::Transaction.current&.labels || {} end diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index 435b74806e7..c2be7f3d63a 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -2,23 +2,38 @@ module Gitlab module GlRepository - def self.gl_repository(project, is_wiki) - "#{is_wiki ? 'wiki' : 'project'}-#{project.id}" + PROJECT = RepoType.new( + name: :project, + access_checker_class: Gitlab::GitAccess, + repository_accessor: -> (project) { project.repository } + ).freeze + WIKI = RepoType.new( + name: :wiki, + access_checker_class: Gitlab::GitAccessWiki, + repository_accessor: -> (project) { project.wiki.repository } + ).freeze + + TYPES = { + PROJECT.name.to_s => PROJECT, + WIKI.name.to_s => WIKI + }.freeze + + def self.types + TYPES end - # rubocop: disable CodeReuse/ActiveRecord def self.parse(gl_repository) - match_data = /\A(project|wiki)-([1-9][0-9]*)\z/.match(gl_repository) - unless match_data + type_name, _id = gl_repository.split('-').first + type = types[type_name] + subject_id = type&.fetch_id(gl_repository) + + unless subject_id raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\"" end - type, id = match_data.captures - project = Project.find_by(id: id) - wiki = type == 'wiki' + project = Project.find_by_id(subject_id) - [project, wiki] + [project, type] end - # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb new file mode 100644 index 00000000000..7abe6c29a25 --- /dev/null +++ b/lib/gitlab/gl_repository/repo_type.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module GlRepository + class RepoType + attr_reader :name, + :access_checker_class, + :repository_accessor + + def initialize(name:, access_checker_class:, repository_accessor:) + @name = name + @access_checker_class = access_checker_class + @repository_accessor = repository_accessor + end + + def identifier_for_subject(subject) + "#{name}-#{subject.id}" + end + + def fetch_id(identifier) + match = /\A#{name}-(?<id>\d+)\z/.match(identifier) + match[:id] if match + end + + def wiki? + self == WIKI + end + + def project? + self == PROJECT + end + + def path_suffix + project? ? "" : ".#{name}" + end + + def repository_for(subject) + repository_accessor.call(subject) + end + end + end +end diff --git a/lib/gitlab/grape_logging/loggers/perf_logger.rb b/lib/gitlab/grape_logging/loggers/perf_logger.rb index e3b9c59bd6e..18ea3a8d2f3 100644 --- a/lib/gitlab/grape_logging/loggers/perf_logger.rb +++ b/lib/gitlab/grape_logging/loggers/perf_logger.rb @@ -6,7 +6,10 @@ module Gitlab module Loggers class PerfLogger < ::GrapeLogging::Loggers::Base def parameters(_, _) - { gitaly_calls: Gitlab::GitalyClient.get_request_count } + { + gitaly_calls: Gitlab::GitalyClient.get_request_count, + gitaly_duration: Gitlab::GitalyClient.query_time_ms + } end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index a0aab9fcbaf..89667976217 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -33,6 +33,7 @@ project_tree: - :user - merge_requests: - :metrics + - :suggestions - notes: - :author - events: diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 202d310e237..207a80b7db2 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -5,19 +5,26 @@ module Gitlab NotFoundError = Class.new(StandardError) def self.parse(repo_path) - wiki = false project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '') - project, was_redirected = find_project(project_path) - - if project_path.end_with?('.wiki') && project.nil? - project, was_redirected = find_project(project_path.chomp('.wiki')) - wiki = true + # Detect the repo type based on the path, the first one tried is the project + # type, which does not have a suffix. + Gitlab::GlRepository.types.each do |_name, type| + # If the project path does not end with the defined suffix, try the next + # type. + # We'll always try to find a project with an empty suffix (for the + # `Gitlab::GlRepository::PROJECT` type. + next unless project_path.end_with?(type.path_suffix) + + project, was_redirected = find_project(project_path.chomp(type.path_suffix)) + redirected_path = project_path if was_redirected + + # If we found a matching project, then the type was matched, no need to + # continue looking. + return [project, type, redirected_path] if project end - redirected_path = project_path if was_redirected - - [project, wiki, redirected_path] + nil end def self.find_project(project_path) diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 265f6213a99..5d5a867c9ab 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -20,14 +20,14 @@ module Gitlab SECRET_LENGTH = 32 class << self - def git_http_ok(repository, is_wiki, user, action, show_all_refs: false) + def git_http_ok(repository, repo_type, user, action, show_all_refs: false) raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s) project = repository.project attrs = { GL_ID: Gitlab::GlId.gl_id(user), - GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki), + GL_REPOSITORY: repo_type.identifier_for_subject(project), GL_USERNAME: user&.username, ShowAllRefs: show_all_refs, Repository: repository.gitaly_repository.to_h, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1a0224a44e6..1e6f04c7815 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -144,6 +144,15 @@ msgstr "" msgid "%{percent}%% complete" msgstr "" +msgid "%{service_title} activated." +msgstr "" + +msgid "%{service_title} settings saved, but not activated." +msgstr "" + +msgid "%{spammable_titlecase} was submitted to Akismet successfully." +msgstr "" + msgid "%{strong_start}%{branch_count}%{strong_end} Branch" msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches" msgstr[0] "" @@ -372,6 +381,9 @@ msgstr "" msgid "Access expiration date" msgstr "" +msgid "Access forbidden. Check your access level." +msgstr "" + msgid "Account" msgstr "" @@ -609,9 +621,18 @@ msgstr "" msgid "All issues for this milestone are closed. You may close this milestone now." msgstr "" +msgid "All merge conflicts were resolved. The merge request can now be merged." +msgstr "" + +msgid "All todos were marked as done." +msgstr "" + msgid "All users" msgstr "" +msgid "All users must have a name." +msgstr "" + msgid "Allow commits from members who can merge to the target branch." msgstr "" @@ -954,6 +975,9 @@ msgstr "" msgid "Authentication method" msgstr "" +msgid "Authentication via U2F device failed." +msgstr "" + msgid "Author" msgstr "" @@ -2447,6 +2471,15 @@ msgstr "" msgid "Copy token to clipboard" msgstr "" +msgid "Could not connect to FogBugz, check your URL" +msgstr "" + +msgid "Could not create Wiki Repository at this time. Please try again later." +msgstr "" + +msgid "Could not remove the trigger." +msgstr "" + msgid "Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}" msgstr "" @@ -3370,6 +3403,9 @@ msgstr "" msgid "Error updating todo status." msgstr "" +msgid "Error uploading file" +msgstr "" + msgid "Error while loading the merge request. Please try again." msgstr "" @@ -3541,6 +3577,9 @@ msgstr "" msgid "Failed to load errors from Sentry. Error message: %{errorMessage}" msgstr "" +msgid "Failed to promote label due to internal error. Please contact administrators." +msgstr "" + msgid "Failed to remove issue from board, please try again." msgstr "" @@ -3786,6 +3825,9 @@ msgstr "" msgid "Git" msgstr "" +msgid "Git LFS is not enabled on this GitLab server, contact your admin." +msgstr "" + msgid "Git global setup" msgstr "" @@ -3966,6 +4008,9 @@ msgstr "" msgid "GroupSettings|Learn more about badges." msgstr "" +msgid "GroupSettings|New runners registration token has been generated!" +msgstr "" + msgid "GroupSettings|Prevent sharing a project within %{group} with other groups" msgstr "" @@ -4130,6 +4175,9 @@ msgstr "" msgid "Hook was successfully created." msgstr "" +msgid "Hook was successfully updated." +msgstr "" + msgid "Housekeeping successfully started" msgstr "" @@ -4409,12 +4457,21 @@ msgstr "" msgid "Introducing Your Conversational Development Index" msgstr "" +msgid "Invalid Login or password" +msgstr "" + +msgid "Invalid file." +msgstr "" + msgid "Invalid input, please avoid emojis" msgstr "" msgid "Invalid pin code" msgstr "" +msgid "Invalid two-factor code." +msgstr "" + msgid "Invitation" msgstr "" @@ -4478,6 +4535,9 @@ msgstr "" msgid "Job has been erased" msgstr "" +msgid "Job has been successfully erased!" +msgstr "" + msgid "Job is stuck. Check runners." msgstr "" @@ -4986,6 +5046,9 @@ msgstr "" msgid "Merged" msgstr "" +msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes." +msgstr "" + msgid "Messages" msgstr "" @@ -5025,6 +5088,9 @@ msgstr "" msgid "Metrics|There was an error getting environments information." msgstr "" +msgid "Metrics|There was an error while retrieving metrics" +msgstr "" + msgid "Metrics|Unexpected deployment data response from prometheus endpoint" msgstr "" @@ -5079,6 +5145,9 @@ msgstr "" msgid "Mirroring repositories" msgstr "" +msgid "Mirroring settings were successfully updated." +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" @@ -5384,6 +5453,9 @@ msgstr "" msgid "Not enough data" msgstr "" +msgid "Not found." +msgstr "" + msgid "Not now" msgstr "" @@ -5489,6 +5561,9 @@ msgstr "" msgid "November" msgstr "" +msgid "Object does not exist on the server or you don't have permissions to access it" +msgstr "" + msgid "Oct" msgstr "" @@ -5590,6 +5665,12 @@ msgstr "" msgid "Owner" msgstr "" +msgid "Page not found" +msgstr "" + +msgid "Page was successfully deleted" +msgstr "" + msgid "Pages" msgstr "" @@ -5752,6 +5833,12 @@ msgstr "" msgid "Pipelines for last year" msgstr "" +msgid "Pipelines page" +msgstr "" + +msgid "Pipelines settings for '%{project_name}' were successfully updated." +msgstr "" + msgid "Pipelines|Build with confidence" msgstr "" @@ -5896,6 +5983,9 @@ msgstr "" msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access." msgstr "" +msgid "Please select a group." +msgstr "" + msgid "Please select at least one filter to see results" msgstr "" @@ -6217,6 +6307,12 @@ msgstr "" msgid "Project \"%{name}\" is no longer available. Select another project to continue." msgstr "" +msgid "Project %{project_repo} could not be found" +msgstr "" + +msgid "Project '%{project_name}' is being imported." +msgstr "" + msgid "Project '%{project_name}' is in the process of being deleted." msgstr "" @@ -6696,6 +6792,12 @@ msgstr "" msgid "Resolved" msgstr "" +msgid "Resolved 1 discussion." +msgstr "" + +msgid "Resolved all discussions." +msgstr "" + msgid "Response metrics (AWS ELB)" msgstr "" @@ -7019,6 +7121,9 @@ msgstr "" msgid "September" msgstr "" +msgid "Server supports batch API only, please update your Git LFS client to version 1.0.1 and up." +msgstr "" + msgid "Server version" msgstr "" @@ -7603,6 +7708,9 @@ msgstr "" msgid "Successfully removed email." msgstr "" +msgid "Successfully scheduled a pipeline to run. Go to the %{link_to_pipelines} for details." +msgstr "" + msgid "Successfully unblocked" msgstr "" @@ -7756,6 +7864,9 @@ msgstr "" msgid "Test coverage parsing" msgstr "" +msgid "Test failed." +msgstr "" + msgid "The Git LFS objects will <strong>not</strong> be synced." msgstr "" @@ -7774,9 +7885,24 @@ msgstr "" msgid "The collection of events added to the data gathered for that stage." msgstr "" +msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." +msgstr "" + msgid "The deployment of this job to %{environmentLink} did not succeed." msgstr "" +msgid "The directory has been successfully created." +msgstr "" + +msgid "The entered user map is not a valid JSON user map." +msgstr "" + +msgid "The file has been successfully created." +msgstr "" + +msgid "The file has been successfully deleted." +msgstr "" + msgid "The fork relationship has been removed." msgstr "" @@ -7789,6 +7915,12 @@ msgstr "" msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgstr "" +msgid "The invitation has already been accepted." +msgstr "" + +msgid "The invitation was successfully resent." +msgstr "" + msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "" @@ -7798,6 +7930,15 @@ msgstr "" msgid "The maximum file size allowed is 200KB." msgstr "" +msgid "The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally." +msgstr "" + +msgid "The merge conflicts for this merge request have already been resolved." +msgstr "" + +msgid "The merge conflicts for this merge request have already been resolved. Please return to the merge request." +msgstr "" + msgid "The name %{entryName} is already taken in this directory." msgstr "" @@ -7822,6 +7963,15 @@ msgstr "" msgid "The project can be accessed without any authentication." msgstr "" +msgid "The project was successfully forked." +msgstr "" + +msgid "The project was successfully imported." +msgstr "" + +msgid "The remote repository is being updated..." +msgstr "" + msgid "The repository for this project does not exist." msgstr "" @@ -7852,12 +8002,18 @@ msgstr "" msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination." msgstr "" +msgid "The uploaded file is not a valid Google Takeout archive." +msgstr "" + msgid "The usage ping is disabled, and cannot be configured through this form." msgstr "" msgid "The user is being deleted." msgstr "" +msgid "The user map has been saved. Continue by selecting the projects you want to import." +msgstr "" + msgid "The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side." msgstr "" @@ -7921,6 +8077,9 @@ msgstr "" msgid "There was an error when unsubscribing from this label." msgstr "" +msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." +msgstr "" + msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue." msgstr "" @@ -8071,6 +8230,9 @@ msgstr "" msgid "This merge request is locked." msgstr "" +msgid "This namespace has already been taken! Please choose another one." +msgstr "" + msgid "This option is disabled as you don't have write permissions for the current branch" msgstr "" @@ -8107,6 +8269,9 @@ msgstr "" msgid "This repository" msgstr "" +msgid "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch." +msgstr "" + msgid "This runner will only run on pipelines triggered on protected branches" msgstr "" @@ -8393,6 +8558,9 @@ msgstr "" msgid "Todo" msgstr "" +msgid "Todo was successfully marked as done." +msgstr "" + msgid "Todos" msgstr "" @@ -8450,6 +8618,9 @@ msgstr "" msgid "Trending" msgstr "" +msgid "Trigger removed." +msgstr "" + msgid "Trigger this manual action" msgstr "" @@ -8459,6 +8630,15 @@ msgstr "" msgid "Trigger variables:" msgstr "" +msgid "Trigger was created successfully." +msgstr "" + +msgid "Trigger was re-assigned." +msgstr "" + +msgid "Trigger was successfully updated." +msgstr "" + msgid "Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions." msgstr "" @@ -8483,9 +8663,15 @@ msgstr "" msgid "Type" msgstr "" +msgid "Unable to connect to server: %{error}" +msgstr "" + msgid "Unable to load the diff. %{button_try_again}" msgstr "" +msgid "Unable to schedule a pipeline to run immediately" +msgstr "" + msgid "Unblock" msgstr "" @@ -8561,6 +8747,9 @@ msgstr "" msgid "Update now" msgstr "" +msgid "Update your bookmarked URLs as filtered/sorted branches URL has been changed." +msgstr "" + msgid "Update your group name, description, avatar, and visibility." msgstr "" @@ -8657,6 +8846,12 @@ msgstr "" msgid "User was successfully created." msgstr "" +msgid "User was successfully removed from group and any subresources." +msgstr "" + +msgid "User was successfully removed from project." +msgstr "" + msgid "User was successfully updated." msgstr "" @@ -8750,6 +8945,9 @@ msgstr "" msgid "Validate your GitLab CI configuration file" msgstr "" +msgid "Validations failed." +msgstr "" + msgid "Value" msgstr "" @@ -8888,6 +9086,9 @@ msgstr "" msgid "Wiki" msgstr "" +msgid "Wiki was successfully updated." +msgstr "" + msgid "WikiClone|Clone your wiki" msgstr "" @@ -9107,6 +9308,12 @@ msgstr "" msgid "You can move around the graph by using the arrow keys." msgstr "" +msgid "You can now submit a merge request to get this change into the original branch." +msgstr "" + +msgid "You can now submit a merge request to get this change into the original project." +msgstr "" + msgid "You can only add files when you are on a branch" msgstr "" @@ -9134,9 +9341,18 @@ msgstr "" msgid "You cannot impersonate an internal user" msgstr "" +msgid "You cannot play this scheduled pipeline at the moment. Please wait a minute." +msgstr "" + msgid "You cannot write to this read-only GitLab instance." msgstr "" +msgid "You could not create a new trigger." +msgstr "" + +msgid "You could not take ownership of trigger." +msgstr "" + msgid "You do not have any subscriptions yet" msgstr "" @@ -9155,6 +9371,9 @@ msgstr "" msgid "You have reached your project limit" msgstr "" +msgid "You left the \"%{membershipable_human_name}\" %{source_type}." +msgstr "" + msgid "You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>." msgstr "" @@ -9170,6 +9389,15 @@ msgstr "" msgid "You need to register a two-factor authentication app before you can set up a U2F device." msgstr "" +msgid "You need to specify both an Access Token and a Host URL." +msgstr "" + +msgid "You need to upload a GitLab project export archive (ending in .gz)." +msgstr "" + +msgid "You need to upload a Google Takeout archive." +msgstr "" + msgid "You will lose all changes you've made to this file. This action cannot be undone." msgstr "" @@ -9242,6 +9470,9 @@ msgstr "" msgid "Your U2F device was registered!" msgstr "" +msgid "Your access request to the %{source_type} has been withdrawn." +msgstr "" + msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO." msgstr "" @@ -9263,6 +9494,9 @@ msgstr "" msgid "Your changes have been saved" msgstr "" +msgid "Your changes have been successfully committed." +msgstr "" + msgid "Your comment will not be visible to the public." msgstr "" @@ -9287,6 +9521,9 @@ msgstr "" msgid "Your projects" msgstr "" +msgid "Your request for access has been queued for review." +msgstr "" + msgid "a deleted user" msgstr "" @@ -9366,6 +9603,9 @@ msgstr "" msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command." msgstr "" +msgid "failed" +msgstr "" + msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}" msgstr "" @@ -9743,6 +9983,9 @@ msgstr "" msgid "stuck" msgstr "" +msgid "success" +msgstr "" + msgid "syntax is correct" msgstr "" diff --git a/package.json b/package.json index b40436d057e..cb063c9782c 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.3.1", - "@gitlab/csslab": "^1.8.0", + "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.54.0", "@gitlab/ui": "^3.0.0", "apollo-boost": "^0.3.1", @@ -40,7 +40,7 @@ "autosize": "^4.0.0", "axios": "^0.17.1", "babel-loader": "^8.0.5", - "bootstrap": "4.1.3", + "bootstrap": "4.3.1", "brace-expansion": "^1.1.8", "cache-loader": "^2.0.1", "chart.js": "2.7.2", @@ -91,7 +91,7 @@ "monaco-editor-webpack-plugin": "^1.7.0", "mousetrap": "^1.4.6", "pikaday": "^1.6.1", - "popper.js": "^1.14.3", + "popper.js": "^1.14.7", "prismjs": "^1.6.0", "prosemirror-markdown": "^1.3.0", "prosemirror-model": "^1.6.4", @@ -118,14 +118,14 @@ "underscore": "^1.9.0", "url-loader": "^1.1.2", "visibilityjs": "^1.2.4", - "vue": "^2.5.21", + "vue": "^2.6.10", "vue-apollo": "^3.0.0-beta.28", - "vue-loader": "^15.4.2", + "vue-loader": "^15.7.0", "vue-resource": "^1.5.1", "vue-router": "^3.0.2", - "vue-template-compiler": "^2.5.21", - "vue-virtual-scroll-list": "^1.2.5", - "vuex": "^3.0.1", + "vue-template-compiler": "^2.6.10", + "vue-virtual-scroll-list": "^1.3.1", + "vuex": "^3.1.0", "webpack": "^4.29.0", "webpack-bundle-analyzer": "^3.0.3", "webpack-cli": "^3.2.1", @@ -152,15 +152,17 @@ "eslint-plugin-import": "^2.14.0", "eslint-plugin-jasmine": "^2.10.1", "eslint-plugin-jest": "^22.3.0", - "gettext-extractor": "^3.3.2", - "gettext-extractor-vue": "^4.0.1", + "gettext-extractor": "^3.4.3", + "gettext-extractor-vue": "^4.0.2", "graphql-tag": "^2.10.0", "istanbul": "^0.4.5", "jasmine-core": "^2.9.0", "jasmine-diff": "^0.1.3", "jasmine-jquery": "^2.1.1", "jest": "^24.1.0", + "jest-environment-jsdom": "^24.0.0", "jest-junit": "^6.3.0", + "jest-util": "^24.0.0", "jsdoc": "^3.5.5", "jsdoc-vue": "^1.0.0", "karma": "^3.0.0", diff --git a/qa/README.md b/qa/README.md index 735868e7640..7d66f7d5abc 100644 --- a/qa/README.md +++ b/qa/README.md @@ -55,16 +55,19 @@ You can also supply specific tests to run as another parameter. For example, to run the repository-related specs, you can execute: ``` -bin/qa Test::Instance::All http://localhost qa/specs/features/repository/ +bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/repository ``` Since the arguments would be passed to `rspec`, you could use all `rspec` options there. For example, passing `--backtrace` and also line number: ``` -bin/qa Test::Instance::All http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace +bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb:6 --backtrace ``` +Note that the separator `--` is required; all subsequent options will be +ignored by the QA framework and passed to `rspec`. + ### Overriding the authenticated user Unless told otherwise, the QA tests will run as the default `root` user seeded @@ -117,7 +120,7 @@ tests that are expected to fail while a fix is in progress (similar to how can be used). ``` -bin/qa Test::Instance::All http://localhost --tag quarantine +bin/qa Test::Instance::All http://localhost -- --tag quarantine ``` If `quarantine` is used with other tags, tests will only be run if they have at @@ -128,3 +131,25 @@ For example, suppose one test has `:smoke` and `:quarantine` metadata, and another test has `:ldap` and `:quarantine` metadata. If the tests are run with `--tag smoke --tag quarantine`, only the first test will run. The test with `:ldap` will not run even though it also has `:quarantine`. + +### Running tests with a feature flag enabled + +Tests can be run with with a feature flag enabled by using the command-line +option `--enable-feature FEATURE_FLAG`. For example, to enable the feature flag +that enforces Gitaly request limits, you would use the command: + +``` +bin/qa Test::Instance::All http://localhost --enable-feature gitaly_enforce_requests_limits +``` + +This will instruct the QA framework to enable the `gitaly_enforce_requests_limits` +feature flag ([via the API](https://docs.gitlab.com/ee/api/features.html)), run +all the tests in the `Test::Instance::All` scenario, and then disable the +feature flag again. + +Note: the QA framework doesn't currently allow you to easily toggle a feature +flag during a single test, [as you can in unit tests](https://docs.gitlab.com/ee/development/feature_flags.html#specs), +but [that capability is planned](https://gitlab.com/gitlab-org/quality/team-tasks/issues/77). + +Note also that the `--` separator isn't used because `--enable-feature` is a QA +framework option, not an `rspec` option.
\ No newline at end of file @@ -17,6 +17,7 @@ module QA autoload :Env, 'qa/runtime/env' autoload :Address, 'qa/runtime/address' autoload :Path, 'qa/runtime/path' + autoload :Feature, 'qa/runtime/feature' autoload :Fixtures, 'qa/runtime/fixtures' autoload :Logger, 'qa/runtime/logger' @@ -89,6 +90,7 @@ module QA autoload :Bootable, 'qa/scenario/bootable' autoload :Actable, 'qa/scenario/actable' autoload :Template, 'qa/scenario/template' + autoload :SharedAttributes, 'qa/scenario/shared_attributes' ## # Test scenario entrypoints. diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 98eebac0880..de04467ff5b 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -8,9 +8,6 @@ module QA module ApiFabricator include Capybara::DSL - HTTP_STATUS_OK = 200 - HTTP_STATUS_CREATED = 201 - ResourceNotFoundError = Class.new(RuntimeError) ResourceFabricationFailedError = Class.new(RuntimeError) ResourceURLMissingError = Class.new(RuntimeError) diff --git a/qa/qa/runtime/address.rb b/qa/qa/runtime/address.rb index ffad3974b02..af0537dc17c 100644 --- a/qa/qa/runtime/address.rb +++ b/qa/qa/runtime/address.rb @@ -15,6 +15,13 @@ module QA @instance.to_s end end + + def self.valid?(value) + uri = URI.parse(value) + uri.is_a?(URI::HTTP) && !uri.host.nil? + rescue URI::InvalidURIError + false + end end end end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index aff84c89f0e..d3327b49339 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -14,7 +14,7 @@ module QA def personal_access_token @personal_access_token ||= begin - # you can set the environment variable PERSONAL_ACCESS_TOKEN + # you can set the environment variable GITLAB_QA_ACCESS_TOKEN # to use a specific access token rather than create one from the UI Runtime::Env.personal_access_token ||= create_personal_access_token end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index dd0ddbdbd6b..03cae3c1fe6 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -53,7 +53,7 @@ module QA # specifies token that can be used for the api def personal_access_token - @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN'] + @personal_access_token ||= ENV['GITLAB_QA_ACCESS_TOKEN'] end def remote_grid diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb new file mode 100644 index 00000000000..1b4ae7adbbe --- /dev/null +++ b/qa/qa/runtime/feature.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Feature + extend self + extend Support::Api + + SetFeatureError = Class.new(RuntimeError) + + def enable(key) + QA::Runtime::Logger.info("Enabling feature: #{key}") + set_feature(key, true) + end + + def disable(key) + QA::Runtime::Logger.info("Disabling feature: #{key}") + set_feature(key, false) + end + + private + + def api_client + @api_client ||= Runtime::API::Client.new(:gitlab) + end + + def set_feature(key, value) + request = Runtime::API::Request.new(api_client, "/features/#{key}") + response = post(request.url, { value: value }) + unless response.code == QA::Support::Api::HTTP_STATUS_CREATED + raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`." + end + end + end + end +end diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb index dd12ea6d492..038418be023 100644 --- a/qa/qa/scenario/bootable.rb +++ b/qa/qa/scenario/bootable.rb @@ -23,7 +23,7 @@ module QA arguments.parse!(argv) - self.perform(Runtime::Scenario.attributes, *arguments.default_argv) + self.perform(Runtime::Scenario.attributes, *argv) end private @@ -33,7 +33,13 @@ module QA end def options - @options ||= [] + # Scenario options/attributes are global. There's only ever one + # scenario at a time, but they can be inherited and we want scenarios + # to share the attributes of their ancestors. For example, `Mattermost` + # inherits from `Test::Instance::All` but if this were an instance + # variable then `Mattermost` wouldn't have access to the attributes + # in `All` + @@options ||= [] # rubocop:disable Style/ClassVars end def has_attributes? diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb new file mode 100644 index 00000000000..40d5c6b1ff1 --- /dev/null +++ b/qa/qa/scenario/shared_attributes.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module QA + module Scenario + module SharedAttributes + include Bootable + + attribute :gitlab_address, '--address URL', 'Address of the instance to test' + attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests' + end + end +end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index cb1a1de6b9a..b8ea26e805e 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -18,19 +18,44 @@ module QA end end - def perform(address, *rspec_options) - Runtime::Scenario.define(:gitlab_address, address) + def perform(options, *args) + extract_address(:gitlab_address, options, args) ## # Perform before hooks, which are different for CE and EE # Runtime::Release.perform_before_hooks + Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature) + Specs::Runner.perform do |specs| specs.tty = true specs.tags = self.class.focus - specs.options = rspec_options if rspec_options.any? + specs.options = args if args.any? end + ensure + Runtime::Feature.disable(options[:enable_feature]) if options.key?(:enable_feature) + end + + def extract_option(name, options, args) + option = if options.key?(name) + options[name] + else + args.shift + end + + Runtime::Scenario.define(name, option) + + option + end + + # For backwards-compatibility, if the gitlab instance address is not + # specified as an option parsed by OptionParser, it can be specified as + # the first argument + def extract_address(name, options, args) + address = extract_option(name, options, args) + + raise ::ArgumentError, "The address provided for `#{name}` is not valid: #{address}" unless Runtime::Address.valid?(address) end end end diff --git a/qa/qa/scenario/test/instance/all.rb b/qa/qa/scenario/test/instance/all.rb index a07c26431bd..168ac4c09a1 100644 --- a/qa/qa/scenario/test/instance/all.rb +++ b/qa/qa/scenario/test/instance/all.rb @@ -8,6 +8,7 @@ module QA module Instance class All < Template include Bootable + include SharedAttributes end end end diff --git a/qa/qa/scenario/test/instance/smoke.rb b/qa/qa/scenario/test/instance/smoke.rb index a7d2cb27f27..43f0623483e 100644 --- a/qa/qa/scenario/test/instance/smoke.rb +++ b/qa/qa/scenario/test/instance/smoke.rb @@ -8,6 +8,7 @@ module QA # class Smoke < Template include Bootable + include SharedAttributes tags :smoke end diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb index ece6fba75c9..f5072ee227c 100644 --- a/qa/qa/scenario/test/integration/mattermost.rb +++ b/qa/qa/scenario/test/integration/mattermost.rb @@ -9,10 +9,13 @@ module QA class Mattermost < Test::Instance::All tags :mattermost - def perform(address, mattermost, *rspec_options) - Runtime::Scenario.define(:mattermost_address, mattermost) + attribute :mattermost_address, '--mattermost-address URL', 'Address of the Mattermost server' - super(address, *rspec_options) + def perform(options, *args) + extract_address(:gitlab_address, options, args) + extract_address(:mattermost_address, options, args) + + super(options, *args) end end end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 0b92ea29ca4..ceb888bb4ef 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -106,8 +106,7 @@ module QA end end - # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/87 - describe 'Auto DevOps', :smoke, :quarantine do + describe 'Auto DevOps', :smoke do it 'enables AutoDevOps by default' do login diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 229bfb44fa5..31cff5a241c 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -1,6 +1,9 @@ module QA module Support module Api + HTTP_STATUS_OK = 200 + HTTP_STATUS_CREATED = 201 + def post(url, payload) RestClient::Request.execute( method: :post, diff --git a/qa/qa/tools/delete_subgroups.rb b/qa/qa/tools/delete_subgroups.rb index c5c48e77ade..3f752adbe6f 100644 --- a/qa/qa/tools/delete_subgroups.rb +++ b/qa/qa/tools/delete_subgroups.rb @@ -3,7 +3,7 @@ require_relative '../../qa' # This script deletes all subgroups of a group specified by ENV['GROUP_NAME_OR_PATH'] -# Required environment variables: PERSONAL_ACCESS_TOKEN and GITLAB_ADDRESS +# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS # Optional environment variable: GROUP_NAME_OR_PATH (defaults to 'gitlab-qa-sandbox-group') # Run `rake delete_subgroups` @@ -14,9 +14,9 @@ module QA def initialize raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] - raise ArgumentError, "Please provide PERSONAL_ACCESS_TOKEN" unless ENV['PERSONAL_ACCESS_TOKEN'] + raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN'] - @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['PERSONAL_ACCESS_TOKEN']) + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN']) end def run diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb index 4fda49c8e48..49a1af8e9f0 100644 --- a/qa/qa/tools/generate_perf_testdata.rb +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -5,7 +5,7 @@ require 'faker' require 'yaml' require_relative '../../qa' # This script generates testdata for Performance Testing. -# Required environment variables: PERSONAL_ACCESS_TOKEN and GITLAB_ADDRESS +# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS # This job creates a urls.txt which contains a hash of all the URLs needed for Performance Testing # Run `rake generate_perf_testdata` @@ -16,9 +16,9 @@ module QA def initialize raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] - raise ArgumentError, "Please provide PERSONAL_ACCESS_TOKEN" unless ENV['PERSONAL_ACCESS_TOKEN'] + raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN'] - @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['PERSONAL_ACCESS_TOKEN']) + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN']) @group_name = "gitlab-qa-perf-sandbox-#{SecureRandom.hex(8)}" @project_name = "my-test-project-#{SecureRandom.hex(8)}" @visibility = "public" diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index fc51f45c3a1..04085efe2ce 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -90,13 +90,13 @@ describe QA::Runtime::Env do described_class.instance_variable_set(:@personal_access_token, nil) end - context 'when PERSONAL_ACCESS_TOKEN is set' do + context 'when GITLAB_QA_ACCESS_TOKEN is set' do before do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + stub_env('GITLAB_QA_ACCESS_TOKEN', 'a_token_too') end it 'returns specified token from env' do - expect(described_class.personal_access_token).to eq 'a_token' + expect(described_class.personal_access_token).to eq 'a_token_too' end end diff --git a/qa/spec/runtime/feature_spec.rb b/qa/spec/runtime/feature_spec.rb new file mode 100644 index 00000000000..192299b7857 --- /dev/null +++ b/qa/spec/runtime/feature_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +describe QA::Runtime::Feature do + let(:api_client) { double('QA::Runtime::API::Client') } + let(:request) { Struct.new(:url).new('http://api') } + let(:response) { Struct.new(:code).new(201) } + + before do + allow(described_class).to receive(:api_client).and_return(api_client) + end + + describe '.enable' do + it 'enables a feature flag' do + expect(QA::Runtime::API::Request) + .to receive(:new) + .with(api_client, "/features/a-flag") + .and_return(request) + expect(described_class) + .to receive(:post) + .with(request.url, { value: true }) + .and_return(response) + + subject.enable('a-flag') + end + end + + describe '.disable' do + it 'disables a feature flag' do + expect(QA::Runtime::API::Request) + .to receive(:new) + .with(api_client, "/features/a-flag") + .and_return(request) + expect(described_class) + .to receive(:post) + .with(request.url, { value: false }) + .and_return(response) + + subject.disable('a-flag') + end + end +end diff --git a/qa/spec/runtime/scenario_spec.rb b/qa/spec/runtime/scenario_spec.rb index 7009192bcc0..70fc71ffc02 100644 --- a/qa/spec/runtime/scenario_spec.rb +++ b/qa/spec/runtime/scenario_spec.rb @@ -13,6 +13,14 @@ describe QA::Runtime::Scenario do .to eq(my_attribute: 'some-value', another_attribute: 'another-value') end + it 'replaces an existing attribute' do + subject.define(:my_attribute, 'some-value') + subject.define(:my_attribute, 'another-value') + + expect(subject.my_attribute).to eq 'another-value' + expect(subject.attributes).to eq(my_attribute: 'another-value') + end + it 'raises error when attribute is not known' do expect { subject.invalid_accessor } .to raise_error ArgumentError, /invalid_accessor/ diff --git a/qa/spec/scenario/bootable_spec.rb b/qa/spec/scenario/bootable_spec.rb index 273aac7677e..bd89b21f7fb 100644 --- a/qa/spec/scenario/bootable_spec.rb +++ b/qa/spec/scenario/bootable_spec.rb @@ -4,14 +4,21 @@ describe QA::Scenario::Bootable do .include(described_class) end + before do + allow(subject).to receive(:options).and_return([]) + allow(QA::Runtime::Scenario).to receive(:attributes).and_return({}) + end + it 'makes it possible to define the scenario attribute' do subject.class_eval do attribute :something, '--something SOMETHING', 'Some attribute' attribute :another, '--another ANOTHER', 'Some other attribute' end + # If we run just this test from the command line it fails unless + # we include the command line args that we use to select this test. expect(subject).to receive(:perform) - .with(something: 'test', another: 'other') + .with({ something: 'test', another: 'other' }) subject.launch!(%w[--another other --something test]) end diff --git a/qa/spec/scenario/template_spec.rb b/qa/spec/scenario/template_spec.rb new file mode 100644 index 00000000000..f97fc22daf9 --- /dev/null +++ b/qa/spec/scenario/template_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +describe QA::Scenario::Template do + let(:feature) { spy('Runtime::Feature') } + let(:release) { spy('Runtime::Release') } + + before do + stub_const('QA::Runtime::Release', release) + stub_const('QA::Runtime::Feature', feature) + allow(QA::Specs::Runner).to receive(:perform) + allow(QA::Runtime::Address).to receive(:valid?).and_return(true) + end + + it 'allows a feature to be enabled' do + subject.perform({ enable_feature: 'a-feature' }) + + expect(feature).to have_received(:enable).with('a-feature') + end + + it 'ensures an enabled feature is disabled afterwards' do + allow(QA::Specs::Runner).to receive(:perform).and_raise('failed test') + + expect { subject.perform({ enable_feature: 'a-feature' }) }.to raise_error('failed test') + + expect(feature).to have_received(:enable).with('a-feature') + expect(feature).to have_received(:disable).with('a-feature') + end +end diff --git a/qa/spec/scenario/test/integration/github_spec.rb b/qa/spec/scenario/test/integration/github_spec.rb index c2aeb1ded1d..6112ba7c694 100644 --- a/qa/spec/scenario/test/integration/github_spec.rb +++ b/qa/spec/scenario/test/integration/github_spec.rb @@ -12,7 +12,7 @@ describe QA::Scenario::Test::Integration::Github do let(:tags) { [:github] } it 'requires a GitHub access token' do - subject.perform('gitlab_address') + subject.perform(args) expect(env).to have_received(:require_github_access_token!) end diff --git a/qa/spec/scenario/test/integration/mattermost_spec.rb b/qa/spec/scenario/test/integration/mattermost_spec.rb index 59caf2ba2cd..4e75e72f4d2 100644 --- a/qa/spec/scenario/test/integration/mattermost_spec.rb +++ b/qa/spec/scenario/test/integration/mattermost_spec.rb @@ -4,14 +4,21 @@ describe QA::Scenario::Test::Integration::Mattermost do context '#perform' do it_behaves_like 'a QA scenario class' do let(:args) { %w[gitlab_address mattermost_address] } + let(:args) do + { + gitlab_address: 'http://gitlab_address', + mattermost_address: 'http://mattermost_address' + } + end + let(:named_options) { %w[--address http://gitlab_address --mattermost-address http://mattermost_address] } let(:tags) { [:mattermost] } let(:options) { ['path1']} it 'requires a GitHub access token' do - subject.perform(*args) + subject.perform(args) expect(attributes).to have_received(:define) - .with(:mattermost_address, 'mattermost_address') + .with(:mattermost_address, 'http://mattermost_address') end end end diff --git a/qa/spec/shared_examples/scenario_shared_examples.rb b/qa/spec/shared_examples/scenario_shared_examples.rb index 5fd55d7d96b..697e6cb39c8 100644 --- a/qa/spec/shared_examples/scenario_shared_examples.rb +++ b/qa/spec/shared_examples/scenario_shared_examples.rb @@ -2,19 +2,23 @@ shared_examples 'a QA scenario class' do let(:attributes) { spy('Runtime::Scenario') } - let(:release) { spy('Runtime::Release') } let(:runner) { spy('Specs::Runner') } + let(:release) { spy('Runtime::Release') } + let(:feature) { spy('Runtime::Feature') } - let(:args) { ['gitlab_address'] } + let(:args) { { gitlab_address: 'http://gitlab_address' } } + let(:named_options) { %w[--address http://gitlab_address] } let(:tags) { [] } let(:options) { %w[path1 path2] } before do + stub_const('QA::Specs::Runner', runner) stub_const('QA::Runtime::Release', release) stub_const('QA::Runtime::Scenario', attributes) - stub_const('QA::Specs::Runner', runner) + stub_const('QA::Runtime::Feature', feature) allow(runner).to receive(:perform).and_yield(runner) + allow(QA::Runtime::Address).to receive(:valid?).and_return(true) end it 'responds to perform' do @@ -22,28 +26,48 @@ shared_examples 'a QA scenario class' do end it 'sets an address of the subject' do - subject.perform(*args) + subject.perform(args) - expect(attributes).to have_received(:define).with(:gitlab_address, 'gitlab_address') + expect(attributes).to have_received(:define).with(:gitlab_address, 'http://gitlab_address').at_least(:once) end it 'performs before hooks' do - subject.perform(*args) + subject.perform(args) expect(release).to have_received(:perform_before_hooks) end it 'sets tags on runner' do - subject.perform(*args) + subject.perform(args) expect(runner).to have_received(:tags=).with(tags) end context 'specifying RSpec options' do it 'sets options on runner' do - subject.perform(*args, *options) + subject.perform(args, *options) expect(runner).to have_received(:options=).with(options) end end + + context 'with named command-line options' do + it 'converts options to attributes' do + described_class.launch!(named_options) + + args do |k, v| + expect(attributes).to have_received(:define).with(k, v) + end + end + + it 'raises an error if the option is invalid' do + expect { described_class.launch!(['--foo']) }.to raise_error(OptionParser::InvalidOption) + end + + it 'passes on options after --' do + expect(described_class).to receive(:perform).with(attributes, *%w[--tag quarantine]) + + described_class.launch!(named_options.push(*%w[-- --tag quarantine])) + end + end end diff --git a/rubocop/cop/migration/update_column_in_batches.rb b/rubocop/cop/migration/update_column_in_batches.rb index 5461abc5ee0..b1c43393f6a 100644 --- a/rubocop/cop/migration/update_column_in_batches.rb +++ b/rubocop/cop/migration/update_column_in_batches.rb @@ -28,7 +28,7 @@ module RuboCop source_name = node.location.expression.source_buffer.name path = Pathname.new(source_name).relative_path_from(rails_root) dirname = File.dirname(path) - .sub(%r{\Adb/(migrate|post_migrate)}, 'spec/migrations') + .sub(%r{db/(migrate|post_migrate)}, 'spec/migrations') filename = File.basename(source_name, '.rb').sub(/\A\d+_/, '') File.join(dirname, "#{filename}_spec.rb") diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb new file mode 100644 index 00000000000..c19a752b07b --- /dev/null +++ b/spec/controllers/graphql_controller_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GraphqlController do + before do + stub_feature_flags(graphql: true) + end + + describe 'POST #execute' do + context 'when user is logged in' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'returns 200 when user can access API' do + post :execute + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns access denied template when user cannot access API' do + # User cannot access API in a couple of cases + # * When user is internal(like ghost users) + # * When user is blocked + expect(Ability).to receive(:allowed?).with(user, :access_api, :global).and_return(false) + + post :execute + + expect(response.status).to eq(403) + expect(response).to render_template('errors/access_denied') + end + end + + context 'when user is not logged in' do + it 'returns 200' do + post :execute + + expect(response).to have_gitlab_http_status(200) + end + end + end +end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index a73f330a7a9..a1809a26265 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -46,10 +46,26 @@ FactoryBot.define do target_branch "improve/awesome" end + trait :merged_last_month do + merged + + after(:build) do |merge_request| + merge_request.build_metrics.merged_at = 1.month.ago + end + end + trait :closed do state :closed end + trait :closed_last_month do + closed + + after(:build) do |merge_request| + merge_request.build_metrics.latest_closed_at = 1.month.ago + end + end + trait :opened do state :opened end diff --git a/spec/factories/suggestions.rb b/spec/factories/suggestions.rb index 307523cc061..b1427e0211f 100644 --- a/spec/factories/suggestions.rb +++ b/spec/factories/suggestions.rb @@ -16,5 +16,11 @@ FactoryBot.define do applied true commit_id { RepoHelpers.sample_commit.id } end + + trait :content_from_repo do + after(:build) do |suggestion, evaluator| + suggestion.from_content = suggestion.fetch_from_content + end + end end end diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb index 5f630c9ffa4..a1fcd4024c0 100644 --- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb @@ -19,6 +19,12 @@ describe "User browses artifacts" do visit(browse_project_job_artifacts_path(project, job)) end + it "renders a link to the job in the breadcrumbs" do + page.within('.js-breadcrumbs-list') do + expect(page).to have_link("##{job.id}", href: project_job_path(project, job)) + end + end + it "shows artifacts" do expect(page).not_to have_selector(".build-sidebar") diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js new file mode 100644 index 00000000000..cb128c7d880 --- /dev/null +++ b/spec/frontend/environment.js @@ -0,0 +1,27 @@ +/* eslint-disable import/no-commonjs */ + +const { ErrorWithStack } = require('jest-util'); +const JSDOMEnvironment = require('jest-environment-jsdom'); + +class CustomEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context); + Object.assign(context.console, { + error(...args) { + throw new ErrorWithStack( + `Unexpected call of console.error() with:\n\n${args.join(', ')}`, + this.error, + ); + }, + + warn(...args) { + throw new ErrorWithStack( + `Unexpected call of console.warn() with:\n\n${args.join(', ')}`, + this.warn, + ); + }, + }); + } +} + +module.exports = CustomEnvironment; diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js index f96f27c4d80..de9058d7832 100644 --- a/spec/frontend/helpers/fixtures.js +++ b/spec/frontend/helpers/fixtures.js @@ -3,8 +3,6 @@ import fs from 'fs'; import path from 'path'; -// jest-util is part of Jest -// eslint-disable-next-line import/no-extraneous-dependencies import { ErrorWithStack } from 'jest-util'; const fixturesBasePath = path.join(process.cwd(), 'spec', 'javascripts', 'fixtures'); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 66c5b17b825..e10193c25b7 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -23,9 +23,6 @@ describe('diff_file_header', () => { }); beforeEach(() => { - gon.features = { - expandDiffFullFile: true, - }; const diffFile = diffDiscussionMock.diff_file; diffFile.added_lines = 2; diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js index 430e8e2baa3..80d6c7fd564 100644 --- a/spec/javascripts/ide/components/error_message_spec.js +++ b/spec/javascripts/ide/components/error_message_spec.js @@ -84,7 +84,7 @@ describe('IDE error message component', () => { expect(vm.isLoading).toBe(true); - vm.$nextTick(() => { + setTimeout(() => { expect(vm.isLoading).toBe(false); done(); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 7ddc734ff56..1e5b55af4ba 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -121,68 +121,48 @@ describe('IDE store file actions', () => { store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line }); - it('calls scrollToTab', done => { - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(scrollToTabSpy).toHaveBeenCalled(); - - done(); - }) - .catch(done.fail); - }); + it('calls scrollToTab', () => { + const dispatch = jasmine.createSpy(); - it('sets the file active', done => { - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(localFile.active).toBeTruthy(); + actions.setFileActive( + { commit() {}, state: store.state, getters: store.getters, dispatch }, + localFile.path, + ); - done(); - }) - .catch(done.fail); + expect(dispatch).toHaveBeenCalledWith('scrollToTab'); }); - it('returns early if file is already active', done => { - localFile.active = true; + it('commits SET_FILE_ACTIVE', () => { + const commit = jasmine.createSpy(); - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(scrollToTabSpy).not.toHaveBeenCalled(); + actions.setFileActive( + { commit, state: store.state, getters: store.getters, dispatch() {} }, + localFile.path, + ); - done(); - }) - .catch(done.fail); + expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { + path: localFile.path, + active: true, + }); }); - it('sets current active file to not active', done => { + it('sets current active file to not active', () => { const f = file('newActive'); store.state.entries[f.path] = f; localFile.active = true; store.state.openFiles.push(localFile); - store - .dispatch('setFileActive', f.path) - .then(() => { - expect(localFile.active).toBeFalsy(); + const commit = jasmine.createSpy(); - done(); - }) - .catch(done.fail); - }); - - it('resets location.hash for line highlighting', done => { - window.location.hash = 'test'; - - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(window.location.hash).not.toBe('test'); + actions.setFileActive( + { commit, state: store.state, getters: store.getters, dispatch() {} }, + f.path, + ); - done(); - }) - .catch(done.fail); + expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { + path: localFile.path, + active: false, + }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 6078a0e7872..454777fa912 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -33,6 +33,11 @@ describe('Dashboard', () => { <div class="layout-page"></div> `); + window.gon = { + ...window.gon, + ee: false, + }; + mock = new MockAdapter(axios); DashboardComponent = Vue.extend(Dashboard); }); @@ -152,6 +157,25 @@ describe('Dashboard', () => { done(); }); }); + + it('hides the dropdown', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + environmentsEndpoint: '', + }, + }); + + Vue.nextTick(() => { + const dropdownIsActiveElement = component.$el.querySelectorAll('.environments'); + + expect(dropdownIsActiveElement.length).toEqual(0); + done(); + }); + }); }); describe('when the window resizes', () => { diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index d716ece3766..ef876dc2941 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -192,9 +192,9 @@ describe('note_app', () => { expect(service.updateNote).toHaveBeenCalled(); // Wait for the requests to finish before destroying - Vue.nextTick() - .then(done) - .catch(done.fail); + setTimeout(() => { + done(); + }); }); }); @@ -227,9 +227,9 @@ describe('note_app', () => { expect(service.updateNote).toHaveBeenCalled(); // Wait for the requests to finish before destroying - Vue.nextTick() - .then(done) - .catch(done.fail); + setTimeout(() => { + done(); + }); }); }); }); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index c48f8188105..b632ee6736d 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -41,8 +41,6 @@ describe('issue_note_form component', () => { noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', noteId: '545', }; - - wrapper = createComponentWrapper(); }); afterEach(() => { @@ -50,6 +48,10 @@ describe('issue_note_form component', () => { }); describe('noteHash', () => { + beforeEach(() => { + wrapper = createComponentWrapper(); + }); + it('returns note hash string based on `noteId`', () => { expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`); }); @@ -71,6 +73,10 @@ describe('issue_note_form component', () => { }); describe('conflicts editing', () => { + beforeEach(() => { + wrapper = createComponentWrapper(); + }); + it('should show conflict message if note changes outside the component', done => { wrapper.setProps({ ...props, @@ -100,6 +106,10 @@ describe('issue_note_form component', () => { }); describe('form', () => { + beforeEach(() => { + wrapper = createComponentWrapper(); + }); + it('should render text area with placeholder', () => { const textarea = wrapper.find('textarea'); @@ -198,10 +208,6 @@ describe('issue_note_form component', () => { }); describe('with autosaveKey', () => { - beforeEach(() => { - wrapper.destroy(); - }); - describe('with draft', () => { beforeEach(done => { Object.assign(props, { diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 3d2232ff239..95717d659b8 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -55,13 +55,16 @@ describe('pipeline graph action component', () => { component.$el.click(); - component - .$nextTick() - .then(() => { - expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); - }) - .then(done) - .catch(done.fail); + setTimeout(() => { + component + .$nextTick() + .then(() => { + expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); + }) + .catch(done.fail); + + done(); + }, 0); }); }); }); diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index dafb892da43..3240e8e4c1b 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -55,7 +55,7 @@ describe('stage column component', () => { id: 4259, name: '<img src=x onerror=alert(document.domain)>', status: { - icon: 'icon_status_success', + icon: 'status_success', label: 'success', tooltip: '<img src=x onerror=alert(document.domain)>', }, diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 3c8b8032de8..19ae7860333 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -120,13 +120,15 @@ describe('Pipelines stage component', () => { setTimeout(() => { component.$el.querySelector('.js-ci-action').click(); - component - .$nextTick() - .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }) - .then(done) - .catch(done.fail); + setTimeout(() => { + component + .$nextTick() + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }) + .then(done) + .catch(done.fail); + }, 0); }, 0); }); }); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js index 030662b4d90..1eb7cb4bd5b 100644 --- a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js +++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js @@ -53,36 +53,32 @@ describe('GkeProjectIdDropdown', () => { }); it('returns default toggle text', done => - vm - .$nextTick() - .then(() => { - vm.setItem(emptyProjectMock); + setTimeout(() => { + vm.setItem(emptyProjectMock); - expect(vm.toggleText).toBe(LABELS.DEFAULT); - done(); - }) - .catch(done.fail)); + expect(vm.toggleText).toBe(LABELS.DEFAULT); + + done(); + })); it('returns project name if project selected', done => - vm - .$nextTick() - .then(() => { - expect(vm.toggleText).toBe(selectedProjectMock.name); - done(); - }) - .catch(done.fail)); + setTimeout(() => { + vm.isLoading = false; + + expect(vm.toggleText).toBe(selectedProjectMock.name); + + done(); + })); it('returns empty toggle text', done => - vm - .$nextTick() - .then(() => { - vm.$store.commit(SET_PROJECTS, null); - vm.setItem(emptyProjectMock); + setTimeout(() => { + vm.$store.commit(SET_PROJECTS, null); + vm.setItem(emptyProjectMock); - expect(vm.toggleText).toBe(LABELS.EMPTY); - done(); - }) - .catch(done.fail)); + expect(vm.toggleText).toBe(LABELS.EMPTY); + + done(); + })); }); describe('selectItem', () => { diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index eced4925489..57b16b12cb0 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -210,6 +210,19 @@ describe('Assignee component', () => { expect(component.$el.querySelector('.user-list-more')).toBe(null); }); + it('sets tooltip container to body', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000', + users, + editable: true, + }, + }).$mount(); + + expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body'); + }); + it('Shows the "show-less" assignees label', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb new file mode 100644 index 00000000000..71fd25df698 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestion_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::Suggestion do + shared_examples 'correct suggestion raw content' do + it 'returns correct raw data' do + expect(suggestion.to_hash).to include(from_content: expected_lines.join, + to_content: "#{text}\n", + lines_above: above, + lines_below: below) + end + end + + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end + let(:diff_file) do + position.diff_file(project.repository) + end + let(:text) { "# parsed suggestion content\n# with comments" } + + def blob_lines_data(from_line, to_line) + diff_file.new_blob_lines_between(from_line, to_line) + end + + def blob_data + blob = diff_file.new_blob + blob.load_all_data! + blob.data + end + + let(:suggestion) do + described_class.new(text, line: line, above: above, below: below, diff_file: diff_file) + end + + describe '#to_hash' do + context 'when changing content surpasses the top limit' do + let(:line) { 4 } + let(:above) { 5 } + let(:below) { 2 } + let(:expected_above) { line - 1 } + let(:expected_below) { below } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + + context 'when changing content surpasses the amount of lines in the blob (bottom)' do + let(:line) { 5 } + let(:above) { 1 } + let(:below) { blob_data.lines.size + 10 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + + context 'when lines are within blob lines boundary' do + let(:line) { 5 } + let(:above) { 2 } + let(:below) { 3 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + + context 'when no extra lines (single-line suggestion)' do + let(:line) { 5 } + let(:above) { 0 } + let(:below) { 0 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + end +end diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb new file mode 100644 index 00000000000..1119ea04995 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::SuggestionsParser do + describe '.parse' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end + + let(:diff_file) do + position.diff_file(project.repository) + end + + subject do + described_class.parse(markdown, project: merge_request.project, + position: position) + end + + def blob_lines_data(from_line, to_line) + diff_file.new_blob_lines_between(from_line, to_line).join + end + + context 'single-line suggestions' do + let(:markdown) do + <<-MARKDOWN.strip_heredoc + ```suggestion + foo + bar + ``` + + ``` + nothing + ``` + + ```suggestion + xpto + baz + ``` + + ```thing + this is not a suggestion, it's a thing + ``` + MARKDOWN + end + + it 'returns a list of Gitlab::Diff::Suggestion' do + expect(subject).to all(be_a(Gitlab::Diff::Suggestion)) + expect(subject.size).to eq(2) + end + + it 'parsed suggestion has correct data' do + from_line, to_line = position.new_line, position.new_line + + expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " foo\n bar\n", + lines_above: 0, + lines_below: 0) + + expect(subject.second.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " xpto\n baz\n", + lines_above: 0, + lines_below: 0) + end + end + end +end diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb new file mode 100644 index 00000000000..f06a2448ff7 --- /dev/null +++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::GlRepository::RepoType do + set(:project) { create(:project) } + + shared_examples 'a repo type' do + describe "#identifier_for_subject" do + subject { described_class.identifier_for_subject(project) } + + it { is_expected.to eq(expected_identifier) } + end + + describe "#fetch_id" do + it "finds an id match in the identifier" do + expect(described_class.fetch_id(expected_identifier)).to eq(expected_id) + end + + it 'does not break on other identifiers' do + expect(described_class.fetch_id("wiki-noid")).to eq(nil) + end + end + + describe "#path_suffix" do + subject { described_class.path_suffix } + + it { is_expected.to eq(expected_suffix) } + end + + describe "#repository_for" do + it "finds the repository for the repo type" do + expect(described_class.repository_for(project)).to eq(expected_repository) + end + end + end + + describe Gitlab::GlRepository::PROJECT do + it_behaves_like 'a repo type' do + let(:expected_identifier) { "project-#{project.id}" } + let(:expected_id) { project.id.to_s } + let(:expected_suffix) { "" } + let(:expected_repository) { project.repository } + end + + it "knows its type" do + expect(described_class).not_to be_wiki + expect(described_class).to be_project + end + end + + describe Gitlab::GlRepository::WIKI do + it_behaves_like 'a repo type' do + let(:expected_identifier) { "wiki-#{project.id}" } + let(:expected_id) { project.id.to_s } + let(:expected_suffix) { ".wiki" } + let(:expected_repository) { project.wiki.repository } + end + + it "knows its type" do + expect(described_class).to be_wiki + expect(described_class).not_to be_project + end + end +end diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb index 4e09020471b..d4b6c629659 100644 --- a/spec/lib/gitlab/gl_repository_spec.rb +++ b/spec/lib/gitlab/gl_repository_spec.rb @@ -5,11 +5,11 @@ describe ::Gitlab::GlRepository do set(:project) { create(:project, :repository) } it 'parses a project gl_repository' do - expect(described_class.parse("project-#{project.id}")).to eq([project, false]) + expect(described_class.parse("project-#{project.id}")).to eq([project, Gitlab::GlRepository::PROJECT]) end it 'parses a wiki gl_repository' do - expect(described_class.parse("wiki-#{project.id}")).to eq([project, true]) + expect(described_class.parse("wiki-#{project.id}")).to eq([project, Gitlab::GlRepository::WIKI]) end it 'throws an argument error on an invalid gl_repository' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 5299ab297f6..e418516569a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -101,6 +101,7 @@ merge_requests: - latest_merge_request_diff - merge_request_pipelines - merge_request_assignees +- suggestions merge_request_diff: - merge_request - merge_request_diff_commits @@ -352,3 +353,5 @@ resource_label_events: - label error_tracking_setting: - project +suggestions: +- note diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ee96e5c4d42..496567b0036 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -608,3 +608,14 @@ ErrorTracking::ProjectErrorTrackingSetting: - project_id - project_name - organization_name +Suggestion: +- id +- note_id +- relative_order +- applied +- commit_id +- from_content +- to_content +- outdated +- lines_above +- lines_below diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 13940713dfc..4c7ca4e2b57 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -6,43 +6,47 @@ describe ::Gitlab::RepoPath do context 'a repository storage path' do it 'parses a full repository path' do - expect(described_class.parse(project.repository.full_path)).to eq([project, false, nil]) + expect(described_class.parse(project.repository.full_path)).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end it 'parses a full wiki path' do - expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, true, nil]) + expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, Gitlab::GlRepository::WIKI, nil]) end end context 'a relative path' do it 'parses a relative repository path' do - expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil]) + expect(described_class.parse(project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end it 'parses a relative wiki path' do - expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil]) + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, nil]) end it 'parses a relative path starting with /' do - expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil]) + expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end context 'of a redirected project' do let(:redirect) { project.route.create_redirect('foo/bar') } it 'parses a relative repository path' do - expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar']) + expect(described_class.parse(redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) end it 'parses a relative wiki path' do - expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki']) + expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, 'foo/bar.wiki']) end it 'parses a relative path starting with /' do - expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar']) + expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) end end end + + it "returns nil for non existent paths" do + expect(described_class.parse("path/non-existent.git")).to eq(nil) + end end describe '.find_project' do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7213eee5675..d88086b01b1 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -250,11 +250,11 @@ describe Gitlab::Workhorse do } end - subject { described_class.git_http_ok(repository, false, user, action) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) } it { expect(subject).to include(params) } - context 'when is_wiki' do + context 'when the repo_type is a wiki' do let(:params) do { GL_ID: "user-#{user.id}", @@ -264,7 +264,7 @@ describe Gitlab::Workhorse do } end - subject { described_class.git_http_ok(repository, true, user, action) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::WIKI, user, action) } it { expect(subject).to include(params) } end @@ -304,7 +304,7 @@ describe Gitlab::Workhorse do end context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } it { is_expected.to include(ShowAllRefs: true) } end @@ -322,7 +322,7 @@ describe Gitlab::Workhorse do it { expect(subject).to include(gitaly_params) } context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } it { is_expected.to include(ShowAllRefs: true) } end diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index bf425a2617c..054ed0be240 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -107,7 +107,7 @@ describe Clusters::Applications::Knative do subject { knative.install_command } it 'should be initialized with latest version' do - expect(subject.version).to eq('0.2.2') + expect(subject.version).to eq('0.3.0') end it_behaves_like 'a command' diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index a3451c67bd8..bc937368cff 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -1,6 +1,22 @@ require 'spec_helper' describe GroupMember do + describe '.count_users_by_group_id' do + it 'counts users by group ID' do + user_1 = create(:user) + user_2 = create(:user) + group_1 = create(:group) + group_2 = create(:group) + + group_1.add_owner(user_1) + group_1.add_owner(user_2) + group_2.add_owner(user_1) + + expect(described_class.count_users_by_group_id).to eq(group_1.id => 2, + group_2.id => 1) + end + end + describe '.access_level_roles' do it 'returns Gitlab::Access.options_with_owner' do expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner) diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 53f5307ea0b..0f00ea7e85e 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -51,7 +51,104 @@ describe MergeRequestDiff do end end - describe '#latest' do + describe '.ids_for_external_storage_migration' do + set(:merge_request) { create(:merge_request) } + set(:outdated) { merge_request.merge_request_diff } + set(:latest) { merge_request.create_merge_request_diff } + + set(:closed_mr) { create(:merge_request, :closed_last_month) } + let(:closed) { closed_mr.merge_request_diff } + + set(:merged_mr) { create(:merge_request, :merged_last_month) } + let(:merged) { merged_mr.merge_request_diff } + + set(:recently_closed_mr) { create(:merge_request, :closed) } + let(:closed_recently) { recently_closed_mr.merge_request_diff } + + set(:recently_merged_mr) { create(:merge_request, :merged) } + let(:merged_recently) { recently_merged_mr.merge_request_diff } + + before do + merge_request.update!(latest_merge_request_diff: latest) + end + + subject { described_class.ids_for_external_storage_migration(limit: 1000) } + + context 'external diffs are disabled' do + before do + stub_external_diffs_setting(enabled: false) + end + + it { is_expected.to be_empty } + end + + context 'external diffs are misconfigured' do + before do + stub_external_diffs_setting(enabled: true, when: 'every second tuesday') + end + + it { is_expected.to be_empty } + end + + context 'external diffs are enabled unconditionally' do + before do + stub_external_diffs_setting(enabled: true) + end + + it { is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, closed_recently.id, merged_recently.id) } + end + + context 'external diffs are enabled for outdated diffs' do + before do + stub_external_diffs_setting(enabled: true, when: 'outdated') + end + + it 'returns records for outdated merge request versions' do + is_expected.to contain_exactly(outdated.id, closed.id, merged.id) + end + end + + context 'with limit' do + it 'respects the limit' do + stub_external_diffs_setting(enabled: true) + + expect(described_class.ids_for_external_storage_migration(limit: 3).count).to eq(3) + end + end + end + + describe '#migrate_files_to_external_storage!' do + let(:diff) { create(:merge_request).merge_request_diff } + + it 'converts from in-database to external storage' do + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + expect(diff).to receive(:save!) + + diff.migrate_files_to_external_storage! + + expect(diff).to be_stored_externally + end + + it 'does nothing with an external diff' do + stub_external_diffs_setting(enabled: true) + + expect(diff).to be_stored_externally + expect(diff).not_to receive(:save!) + + diff.migrate_files_to_external_storage! + end + + it 'does nothing if external diffs are disabled' do + expect(diff).not_to be_stored_externally + expect(diff).not_to receive(:save!) + + diff.migrate_files_to_external_storage! + end + end + + describe '#latest?' do let!(:mr) { create(:merge_request, :with_diffs) } let!(:first_diff) { mr.merge_request_diff } let!(:last_diff) { mr.create_merge_request_diff } @@ -222,14 +319,58 @@ describe MergeRequestDiff do include_examples 'merge request diffs' end - describe 'external diffs configured' do + describe 'external diffs always enabled' do before do - stub_external_diffs_setting(enabled: true) + stub_external_diffs_setting(enabled: true, when: 'always') end include_examples 'merge request diffs' end + describe 'exernal diffs enabled for outdated diffs' do + before do + stub_external_diffs_setting(enabled: true, when: 'outdated') + end + + include_examples 'merge request diffs' + + it 'stores up-to-date diffs in the database' do + expect(diff).not_to be_stored_externally + end + + it 'stores diffs for recently closed MRs in the database' do + mr = create(:merge_request, :closed) + + expect(mr.merge_request_diff).not_to be_stored_externally + end + + it 'stores diffs for recently merged MRs in the database' do + mr = create(:merge_request, :merged) + + expect(mr.merge_request_diff).not_to be_stored_externally + end + + it 'stores diffs for old MR versions in external storage' do + old_diff = diff + merge_request.create_merge_request_diff + old_diff.migrate_files_to_external_storage! + + expect(old_diff).to be_stored_externally + end + + it 'stores diffs for old closed MRs in external storage' do + mr = create(:merge_request, :closed_last_month) + + expect(mr.merge_request_diff).to be_stored_externally + end + + it 'stores diffs for old merged MRs in external storage' do + mr = create(:merge_request, :merged_last_month) + + expect(mr.merge_request_diff).to be_stored_externally + end + end + describe '#commit_shas' do it 'returns all commit SHAs using commits from the DB' do expect(diff_with_commits.commit_shas).not_to be_empty diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1ea54eeb4f7..90dcf861849 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2710,7 +2710,7 @@ describe Project do end describe '#any_lfs_file_locks?', :request_store do - set(:project) { create(:project) } + let!(:project) { create(:project) } it 'returns false when there are no LFS file locks' do expect(project.any_lfs_file_locks?).to be_falsey @@ -3148,6 +3148,53 @@ describe Project do expect(projects).to eq([public_project]) end end + + context 'with requested visibility levels' do + set(:internal_project) { create(:project, :internal, :repository) } + set(:private_project_2) { create(:project, :private) } + + context 'with admin user' do + set(:admin) { create(:admin) } + + it 'returns all projects' do + projects = described_class.all.public_or_visible_to_user(admin, []) + + expect(projects).to match_array([public_project, private_project, private_project_2, internal_project]) + end + + it 'returns all public and private projects' do + projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to match_array([public_project, private_project, private_project_2]) + end + + it 'returns all private projects' do + projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to match_array([private_project, private_project_2]) + end + end + + context 'with regular user' do + it 'returns authorized projects' do + projects = described_class.all.public_or_visible_to_user(user, []) + + expect(projects).to match_array([public_project, private_project, internal_project]) + end + + it "returns user's public and private projects" do + projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to match_array([public_project, private_project]) + end + + it 'returns one private project' do + projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to eq([private_project]) + end + end + end end describe '.with_feature_available_for_user' do @@ -3428,7 +3475,7 @@ describe Project do end it 'schedules HashedStorage::ProjectMigrateWorker with delayed start when the project repo is in use' do - Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: false)).increase + Gitlab::ReferenceCounter.new(Gitlab::GlRepository::PROJECT.identifier_for_subject(project)).increase expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_in) @@ -3436,7 +3483,7 @@ describe Project do end it 'schedules HashedStorage::ProjectMigrateWorker with delayed start when the wiki repo is in use' do - Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: true)).increase + Gitlab::ReferenceCounter.new(Gitlab::GlRepository::WIKI.identifier_for_subject(project)).increase expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_in) @@ -3569,16 +3616,6 @@ describe Project do end end - describe '#gl_repository' do - let(:project) { create(:project) } - - it 'delegates to Gitlab::GlRepository.gl_repository' do - expect(Gitlab::GlRepository).to receive(:gl_repository).with(project, true) - - project.gl_repository(is_wiki: true) - end - end - describe '#has_ci?' do set(:project) { create(:project) } let(:repository) { double } diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb index 4b76d65ef69..52c23951e37 100644 --- a/spec/policies/board_policy_spec.rb +++ b/spec/policies/board_policy_spec.rb @@ -17,14 +17,6 @@ describe BoardPolicy do ] end - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end - context 'group board' do subject { described_class.new(user, group_board) } diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 92bdaa8b8b8..dc98baca6dc 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -1,63 +1,7 @@ require 'spec_helper' describe GroupPolicy do - let(:guest) { create(:user) } - let(:reporter) { create(:user) } - let(:developer) { create(:user) } - let(:maintainer) { create(:user) } - let(:owner) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group, :private) } - - let(:guest_permissions) do - [:read_label, :read_group, :upload_file, :read_namespace, :read_group_activity, - :read_group_issues, :read_group_boards, :read_group_labels, :read_group_milestones, - :read_group_merge_requests] - end - - let(:reporter_permissions) { [:admin_label] } - - let(:developer_permissions) { [:admin_milestone] } - - let(:maintainer_permissions) do - [ - :create_projects, - :read_cluster, - :create_cluster, - :update_cluster, - :admin_cluster, - :add_cluster - ] - end - - let(:owner_permissions) do - [ - :admin_group, - :admin_namespace, - :admin_group_member, - :change_visibility_level, - :set_note_created_at, - (Gitlab::Database.postgresql? ? :create_subgroup : nil) - ].compact - end - - before do - group.add_guest(guest) - group.add_reporter(reporter) - group.add_developer(developer) - group.add_maintainer(maintainer) - group.add_owner(owner) - end - - subject { described_class.new(current_user, group) } - - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end + include_context 'GroupPolicy context' context 'with no user' do let(:group) { create(:group, :public) } diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb index 1fdf95ad716..99fa8b1fe44 100644 --- a/spec/policies/namespace_policy_spec.rb +++ b/spec/policies/namespace_policy_spec.rb @@ -30,7 +30,7 @@ describe NamespacePolicy do context 'user who has exceeded project limit' do let(:owner) { create(:user, projects_limit: 0) } - it { is_expected.not_to be_allowed(:create_projects) } + it { is_expected.to be_disallowed(:create_projects) } end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 772d1fbee2b..726ccba8807 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1,96 +1,7 @@ require 'spec_helper' describe ProjectPolicy do - set(:guest) { create(:user) } - set(:reporter) { create(:user) } - set(:developer) { create(:user) } - set(:maintainer) { create(:user) } - set(:owner) { create(:user) } - set(:admin) { create(:admin) } - let(:project) { create(:project, :public, namespace: owner.namespace) } - - let(:base_guest_permissions) do - %i[ - read_project read_board read_list read_wiki read_issue - read_project_for_iids read_issue_iid read_label - read_milestone read_project_snippet read_project_member read_note - create_project create_issue create_note upload_file create_merge_request_in - award_emoji read_release - ] - end - - let(:base_reporter_permissions) do - %i[ - download_code fork_project create_project_snippet update_issue - admin_issue admin_label admin_list read_commit_status read_build - read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue - ] - end - - let(:team_member_reporter_permissions) do - %i[build_download_code build_read_container_image] - end - - let(:developer_permissions) do - %i[ - admin_milestone admin_merge_request update_merge_request create_commit_status - update_commit_status create_build update_build create_pipeline - update_pipeline create_merge_request_from create_wiki push_code - resolve_note create_container_image update_container_image - create_environment create_deployment create_release update_release - ] - end - - let(:base_maintainer_permissions) do - %i[ - push_to_delete_protected_branch update_project_snippet update_environment - update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project - admin_commit_status admin_build admin_container_image - admin_pipeline admin_environment admin_deployment destroy_release add_cluster - daily_statistics - ] - end - - let(:public_permissions) do - %i[ - download_code fork_project read_commit_status read_pipeline - read_container_image build_download_code build_read_container_image - download_wiki_code read_release - ] - end - - let(:owner_permissions) do - %i[ - change_namespace change_visibility_level rename_project remove_project - archive_project remove_fork_project destroy_merge_request destroy_issue - set_issue_iid set_issue_created_at set_note_created_at - ] - end - - # Used in EE specs - let(:additional_guest_permissions) { [] } - let(:additional_reporter_permissions) { [] } - let(:additional_maintainer_permissions) { [] } - - let(:guest_permissions) { base_guest_permissions + additional_guest_permissions } - let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions } - let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions } - - before do - project.add_guest(guest) - project.add_maintainer(maintainer) - project.add_developer(developer) - project.add_reporter(reporter) - end - - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end + include_context 'ProjectPolicy context' it 'does not include the read_issue permission when the issue author is not a member of the private project' do project = create(:project, :private) @@ -140,7 +51,7 @@ describe ProjectPolicy do end it 'disables boards and lists permissions' do - expect_disallowed :read_board, :create_board, :update_board, :admin_board + expect_disallowed :read_board, :create_board, :update_board expect_disallowed :read_list, :create_list, :update_list, :admin_list end @@ -237,237 +148,6 @@ describe ProjectPolicy do end end - shared_examples 'archived project policies' do - let(:feature_write_abilities) do - described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| - described_class.create_update_admin_destroy(feature) - end - end - - let(:other_write_abilities) do - %i[ - create_merge_request_in - create_merge_request_from - push_to_delete_protected_branch - push_code - request_access - upload_file - resolve_note - award_emoji - ] - end - - context 'when the project is archived' do - before do - project.archived = true - end - - it 'disables write actions on all relevant project features' do - expect_disallowed(*feature_write_abilities) - end - - it 'disables some other important write actions' do - expect_disallowed(*other_write_abilities) - end - - it 'does not disable other abilities' do - expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities)) - end - end - end - - shared_examples 'project policies as anonymous' do - context 'abilities for public projects' do - context 'when a project has pending invites' do - let(:group) { create(:group, :public) } - let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } - let(:anonymous_permissions) { guest_permissions - user_permissions } - - subject { described_class.new(nil, project) } - - before do - create(:group_member, :invited, group: group) - end - - it 'does not grant owner access' do - expect_allowed(*anonymous_permissions) - expect_disallowed(*user_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { anonymous_permissions } - end - end - end - - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(nil, project) } - - it { is_expected.to be_banned } - end - end - - shared_examples 'project policies as guest' do - subject { described_class.new(guest, project) } - - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - let(:reporter_public_build_permissions) do - reporter_permissions - [:read_build, :read_pipeline] - end - - it do - expect_allowed(*guest_permissions) - expect_disallowed(*reporter_public_build_permissions) - expect_disallowed(*team_member_reporter_permissions) - expect_disallowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { guest_permissions } - end - - context 'public builds enabled' do - it do - expect_allowed(*guest_permissions) - expect_allowed(:read_build, :read_pipeline) - end - end - - context 'when public builds disabled' do - before do - project.update(public_builds: false) - end - - it do - expect_allowed(*guest_permissions) - expect_disallowed(:read_build, :read_pipeline) - end - end - - context 'when builds are disabled' do - before do - project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) - end - - it do - expect_disallowed(:read_build) - expect_allowed(:read_pipeline) - end - end - end - end - - shared_examples 'project policies as reporter' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(reporter, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_disallowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { reporter_permissions } - end - end - end - - shared_examples 'project policies as developer' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(developer, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { developer_permissions } - end - end - end - - shared_examples 'project policies as maintainer' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(maintainer, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_allowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { maintainer_permissions } - end - end - end - - shared_examples 'project policies as owner' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(owner, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_allowed(*maintainer_permissions) - expect_allowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { owner_permissions } - end - end - end - - shared_examples 'project policies as admin' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(admin, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_disallowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_allowed(*maintainer_permissions) - expect_allowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { owner_permissions } - end - end - end - it_behaves_like 'project policies as anonymous' it_behaves_like 'project policies as guest' it_behaves_like 'project policies as reporter' diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index d6329e84579..2e9ef1e89fd 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -5,7 +5,7 @@ describe ProjectSnippetPolicy do let(:regular_user) { create(:user) } let(:external_user) { create(:user, :external) } let(:project) { create(:project, :public) } - + let(:snippet) { create(:project_snippet, snippet_visibility, project: project) } let(:author_permissions) do [ :update_project_snippet, @@ -13,23 +13,13 @@ describe ProjectSnippetPolicy do ] end - def abilities(user, snippet_visibility) - snippet = create(:project_snippet, snippet_visibility, project: project) - - described_class.new(user, snippet) - end - - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end + subject { described_class.new(current_user, snippet) } context 'public snippet' do + let(:snippet_visibility) { :public } + context 'no user' do - subject { abilities(nil, :public) } + let(:current_user) { nil } it do expect_allowed(:read_project_snippet) @@ -38,7 +28,7 @@ describe ProjectSnippetPolicy do end context 'regular user' do - subject { abilities(regular_user, :public) } + let(:current_user) { regular_user } it do expect_allowed(:read_project_snippet, :create_note) @@ -47,7 +37,7 @@ describe ProjectSnippetPolicy do end context 'external user' do - subject { abilities(external_user, :public) } + let(:current_user) { external_user } it do expect_allowed(:read_project_snippet, :create_note) @@ -57,8 +47,10 @@ describe ProjectSnippetPolicy do end context 'internal snippet' do + let(:snippet_visibility) { :internal } + context 'no user' do - subject { abilities(nil, :internal) } + let(:current_user) { nil } it do expect_disallowed(:read_project_snippet) @@ -67,7 +59,7 @@ describe ProjectSnippetPolicy do end context 'regular user' do - subject { abilities(regular_user, :internal) } + let(:current_user) { regular_user } it do expect_allowed(:read_project_snippet, :create_note) @@ -76,31 +68,31 @@ describe ProjectSnippetPolicy do end context 'external user' do - subject { abilities(external_user, :internal) } + let(:current_user) { external_user } it do expect_disallowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end - end - context 'project team member external user' do - subject { abilities(external_user, :internal) } - - before do - project.add_developer(external_user) - end + context 'project team member' do + before do + project.add_developer(external_user) + end - it do - expect_allowed(:read_project_snippet, :create_note) - expect_disallowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_disallowed(*author_permissions) + end end end end context 'private snippet' do + let(:snippet_visibility) { :private } + context 'no user' do - subject { abilities(nil, :private) } + let(:current_user) { nil } it do expect_disallowed(:read_project_snippet) @@ -109,53 +101,52 @@ describe ProjectSnippetPolicy do end context 'regular user' do - subject { abilities(regular_user, :private) } + let(:current_user) { regular_user } it do expect_disallowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end - end - - context 'snippet author' do - let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } - subject { described_class.new(regular_user, snippet) } + context 'snippet author' do + let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } - it do - expect_allowed(:read_project_snippet, :create_note) - expect_allowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_allowed(*author_permissions) + end end - end - context 'project team member normal user' do - subject { abilities(regular_user, :private) } - - before do - project.add_developer(regular_user) - end + context 'project team member normal user' do + before do + project.add_developer(regular_user) + end - it do - expect_allowed(:read_project_snippet, :create_note) - expect_disallowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_disallowed(*author_permissions) + end end end - context 'project team member external user' do - subject { abilities(external_user, :private) } + context 'external user' do + context 'project team member' do + let(:current_user) { external_user } - before do - project.add_developer(external_user) - end + before do + project.add_developer(external_user) + end - it do - expect_allowed(:read_project_snippet, :create_note) - expect_disallowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_disallowed(*author_permissions) + end end end context 'admin user' do - subject { abilities(create(:admin), :private) } + let(:snippet_visibility) { :private } + let(:current_user) { create(:admin) } it do expect_allowed(:read_project_snippet, :create_note) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index b184c92824a..537194b8e11 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -321,7 +321,7 @@ describe API::Internal do end context 'with env passed as a JSON' do - let(:gl_repository) { project.gl_repository(is_wiki: true) } + let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_subject(project) } it 'sets env in RequestStore' do obj_dir_relative = './objects' @@ -975,9 +975,9 @@ describe API::Internal do def gl_repository_for(project_or_wiki) case project_or_wiki when ProjectWiki - project_or_wiki.project.gl_repository(is_wiki: true) + Gitlab::GlRepository::WIKI.identifier_for_subject(project_or_wiki.project) when Project - project_or_wiki.gl_repository(is_wiki: false) + Gitlab::GlRepository::PROJECT.identifier_for_subject(project_or_wiki) else nil end diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb index 3c2842e5725..5b07e598b8d 100644 --- a/spec/requests/api/suggestions_spec.rb +++ b/spec/requests/api/suggestions_spec.rb @@ -42,8 +42,7 @@ describe API::Suggestions do expect(response).to have_gitlab_http_status(200) expect(json_response) - .to include('id', 'from_original_line', 'to_original_line', - 'from_line', 'to_line', 'appliable', 'applied', + .to include('id', 'from_line', 'to_line', 'appliable', 'applied', 'from_content', 'to_content') end end diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb index 1c8ab0ad5d2..cba01400d85 100644 --- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb +++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb @@ -93,4 +93,22 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do it_behaves_like 'a migration file with no spec file' it_behaves_like 'a migration file with a spec file' end + + context 'EE migrations' do + let(:spec_filepath) { tmp_rails_root.join('ee', 'spec', 'migrations', 'my_super_migration_spec.rb') } + + context 'in a migration' do + let(:migration_filepath) { tmp_rails_root.join('ee', 'db', 'migrate', '20121220064453_my_super_migration.rb') } + + it_behaves_like 'a migration file with no spec file' + it_behaves_like 'a migration file with a spec file' + end + + context 'in a post migration' do + let(:migration_filepath) { tmp_rails_root.join('ee', 'db', 'post_migrate', '20121220064453_my_super_migration.rb') } + + it_behaves_like 'a migration file with no spec file' + it_behaves_like 'a migration file with a spec file' + end + end end diff --git a/spec/serializers/suggestion_entity_spec.rb b/spec/serializers/suggestion_entity_spec.rb index 047571f161c..d38fc2b132b 100644 --- a/spec/serializers/suggestion_entity_spec.rb +++ b/spec/serializers/suggestion_entity_spec.rb @@ -13,8 +13,8 @@ describe SuggestionEntity do subject { entity.as_json } it 'exposes correct attributes' do - expect(subject).to include(:id, :from_original_line, :to_original_line, :from_line, - :to_line, :appliable, :applied, :from_content, :to_content) + expect(subject).to include(:id, :from_line, :to_line, :appliable, + :applied, :from_content, :to_content) end it 'exposes current user abilities' do diff --git a/spec/services/merge_requests/migrate_external_diffs_service_spec.rb b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb new file mode 100644 index 00000000000..40ac747e66f --- /dev/null +++ b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::MigrateExternalDiffsService do + let(:merge_request) { create(:merge_request) } + let(:diff) { merge_request.merge_request_diff } + + describe '.enqueue!', :sidekiq do + around do |example| + Sidekiq::Testing.fake! { example.run } + end + + it 'enqueues nothing if external diffs are disabled' do + expect(diff).not_to be_stored_externally + + expect { described_class.enqueue! } + .not_to change { MigrateExternalDiffsWorker.jobs.count } + end + + it 'enqueues eligible in-database diffs if external diffs are enabled' do + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + + expect { described_class.enqueue! } + .to change { MigrateExternalDiffsWorker.jobs.count } + .by(1) + end + end + + describe '#execute' do + it 'migrates an in-database diff to the external store' do + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + + described_class.new(diff).execute + + expect(diff).to be_stored_externally + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 43ceb1dcbee..6c8ff163692 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -97,6 +97,15 @@ describe MergeRequests::RefreshService do } end + it 'outdates MR suggestions' do + expect_next_instance_of(Suggestions::OutdateService) do |service| + expect(service).to receive(:execute).with(@merge_request).and_call_original + expect(service).to receive(:execute).with(@another_merge_request).and_call_original + end + + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + end + context 'when source branch ref does not exists' do before do DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) @@ -329,14 +338,16 @@ describe MergeRequests::RefreshService do context 'push to fork repo source branch' do let(:refresh_service) { service.new(@fork_project, @user) } - context 'open fork merge request' do - before do - allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs - end + def refresh + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + context 'open fork merge request' do it 'executes hooks with update action' do + refresh + expect(refresh_service).to have_received(:execute_hooks) .with(@fork_merge_request, 'update', old_rev: @oldrev) @@ -347,21 +358,30 @@ describe MergeRequests::RefreshService do expect(@build_failed_todo).to be_pending expect(@fork_build_failed_todo).to be_pending end + + it 'outdates opened forked MR suggestions' do + expect_next_instance_of(Suggestions::OutdateService) do |service| + expect(service).to receive(:execute).with(@fork_merge_request).and_call_original + end + + refresh + end end context 'closed fork merge request' do before do @fork_merge_request.close! - allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs end it 'do not execute hooks with update action' do + refresh + expect(refresh_service).not_to have_received(:execute_hooks) end it 'updates merge request to closed state' do + refresh + expect(@merge_request.notes).to be_empty expect(@merge_request).to be_open expect(@fork_merge_request.notes).to be_empty diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 6040f9100f8..4b6d0c51363 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -2,29 +2,56 @@ require 'spec_helper' describe Projects::ParticipantsService do describe '#groups' do + set(:user) { create(:user) } + set(:project) { create(:project, :public) } + let(:service) { described_class.new(project, user) } + + it 'avoids N+1 queries' do + group_1 = create(:group) + group_1.add_owner(user) + + service.groups # Run general application warmup queries + control_count = ActiveRecord::QueryRecorder.new { service.groups }.count + + group_2 = create(:group) + group_2.add_owner(user) + + expect { service.groups }.not_to exceed_query_limit(control_count) + end + + it 'returns correct user counts for groups' do + group_1 = create(:group) + group_1.add_owner(user) + group_1.add_owner(create(:user)) + + group_2 = create(:group) + group_2.add_owner(user) + create(:group_member, :access_request, group: group_2, user: create(:user)) + + expect(service.groups).to contain_exactly( + a_hash_including(name: group_1.full_name, count: 2), + a_hash_including(name: group_2.full_name, count: 1) + ) + end + describe 'avatar_url' do - let(:project) { create(:project, :public) } let(:group) { create(:group, avatar: fixture_file_upload('spec/fixtures/dk.png')) } - let(:user) { create(:user) } - let!(:group_member) { create(:group_member, group: group, user: user) } - it 'should return an url for the avatar' do - participants = described_class.new(project, user) - groups = participants.groups + before do + group.add_owner(user) + end - expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png") + it 'should return an url for the avatar' do + expect(service.groups.size).to eq 1 + expect(service.groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png") end it 'should return an url for the avatar with relative url' do stub_config_setting(relative_url_root: '/gitlab') stub_config_setting(url: Settings.send(:build_gitlab_url)) - participants = described_class.new(project, user) - groups = participants.groups - - expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png") + expect(service.groups.size).to eq 1 + expect(service.groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png") end end end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index fe85b5c9065..80b5dcac6c7 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -5,6 +5,41 @@ require 'spec_helper' describe Suggestions::ApplyService do include ProjectForksHelper + shared_examples 'successfully creates commit and updates suggestion' do + def apply(suggestion) + result = subject.execute(suggestion) + expect(result[:status]).to eq(:success) + end + + it 'updates the file with the new contents' do + apply(suggestion) + + blob = project.repository.blob_at_branch(merge_request.source_branch, + position.new_path) + + expect(blob.data).to eq(expected_content) + end + + it 'updates suggestion applied and commit_id columns' do + expect { apply(suggestion) } + .to change(suggestion, :applied) + .from(false).to(true) + .and change(suggestion, :commit_id) + .from(nil) + end + + it 'created commit has users email and name' do + apply(suggestion) + + commit = project.repository.commit + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + expect(commit.author_name).to eq(user.name) + end + end + let(:project) { create(:project, :repository) } let(:user) { create(:user, :commit_email) } @@ -17,9 +52,8 @@ describe Suggestions::ApplyService do end let(:suggestion) do - create(:suggestion, note: diff_note, - from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n", - to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") + create(:suggestion, :content_from_repo, note: diff_note, + to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") end subject { described_class.new(user) } @@ -84,39 +118,7 @@ describe Suggestions::ApplyService do project.add_maintainer(user) end - it 'updates the file with the new contents' do - subject.execute(suggestion) - - blob = project.repository.blob_at_branch(merge_request.source_branch, - position.new_path) - - expect(blob.data).to eq(expected_content) - end - - it 'returns success status' do - result = subject.execute(suggestion) - - expect(result[:status]).to eq(:success) - end - - it 'updates suggestion applied and commit_id columns' do - expect { subject.execute(suggestion) } - .to change(suggestion, :applied) - .from(false).to(true) - .and change(suggestion, :commit_id) - .from(nil) - end - - it 'created commit has users email and name' do - subject.execute(suggestion) - - commit = project.repository.commit - - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) - expect(commit.author_name).to eq(user.name) - end + it_behaves_like 'successfully creates commit and updates suggestion' context 'when it fails to apply because the file was changed' do it 'returns error message' do @@ -212,11 +214,13 @@ describe Suggestions::ApplyService do end def apply_suggestion(suggestion) - suggestion.note.reload + suggestion.reload merge_request.reload merge_request.clear_memoized_shas result = subject.execute(suggestion) + expect(result[:status]).to eq(:success) + refresh = MergeRequests::RefreshService.new(project, user) refresh.execute(merge_request.diff_head_sha, suggestion.commit_id, @@ -241,7 +245,7 @@ describe Suggestions::ApplyService do suggestion_2_changes = { old_line: 24, new_line: 31, - from_content: " @cmd_output << stderr.read\n", + from_content: " @cmd_output << stderr.read\n", to_content: "# v2 change\n", path: path } @@ -368,7 +372,18 @@ describe Suggestions::ApplyService do result = subject.execute(suggestion) - expect(result).to eq(message: 'The file was not found', + expect(result).to eq(message: 'Suggestion is not appliable', + status: :error) + end + end + + context 'suggestion is eligible to be outdated' do + it 'returns error message' do + expect(suggestion).to receive(:outdated?) { true } + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'Suggestion is not appliable', status: :error) end end diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb index 1b4b15b8eaa..ce4990a34a4 100644 --- a/spec/services/suggestions/create_service_spec.rb +++ b/spec/services/suggestions/create_service_spec.rb @@ -40,6 +40,14 @@ describe Suggestions::CreateService do ```thing this is not a suggestion, it's a thing ``` + + ```suggestion:-3+2 + # multi-line suggestion 1 + ``` + + ```suggestion:-5 + # multi-line suggestion 1 + ``` MARKDOWN end @@ -54,7 +62,7 @@ describe Suggestions::CreateService do end it 'does not try to parse suggestions' do - expect(Banzai::SuggestionsParser).not_to receive(:parse) + expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse) subject.execute end @@ -71,7 +79,7 @@ describe Suggestions::CreateService do it 'does not try to parse suggestions' do allow(note).to receive(:on_text?) { false } - expect(Banzai::SuggestionsParser).not_to receive(:parse) + expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse) subject.execute end @@ -87,7 +95,9 @@ describe Suggestions::CreateService do end it 'creates no suggestion when diff file is not found' do - expect(note).to receive(:latest_diff_file) { nil } + expect_next_instance_of(DiffNote) do |diff_note| + expect(diff_note).to receive(:latest_diff_file).twice { nil } + end expect { subject.execute }.not_to change(Suggestion, :count) end @@ -101,43 +111,44 @@ describe Suggestions::CreateService do note: markdown) end - context 'single line suggestions' do - it 'persists suggestion records' do - expect { subject.execute } - .to change { note.suggestions.count } - .from(0) - .to(2) - end + let(:expected_suggestions) do + Gitlab::Diff::SuggestionsParser.parse(markdown, + project: note.project, + position: note.position) + end - it 'persists original from_content lines and suggested lines' do - subject.execute + it 'persists suggestion records' do + expect { subject.execute }.to change { note.suggestions.count } + .from(0).to(expected_suggestions.size) + end - suggestions = note.suggestions.order(:relative_order) + it 'persists suggestions data correctly' do + subject.execute - suggestion_1 = suggestions.first - suggestion_2 = suggestions.last + suggestions = note.suggestions.order(:relative_order) - expect(suggestion_1).to have_attributes(from_content: " vars = {\n", - to_content: " foo\n bar\n") + suggestions.zip(expected_suggestions) do |suggestion, expected_suggestion| + expected_data = expected_suggestion.to_hash - expect(suggestion_2).to have_attributes(from_content: " vars = {\n", - to_content: " xpto\n baz\n") + expect(suggestion.from_content).to eq(expected_data[:from_content]) + expect(suggestion.to_content).to eq(expected_data[:to_content]) + expect(suggestion.lines_above).to eq(expected_data[:lines_above]) + expect(suggestion.lines_below).to eq(expected_data[:lines_below]) end + end - context 'outdated position note' do - let!(:outdated_diff) { merge_request.merge_request_diff } - let!(:latest_diff) { merge_request.create_merge_request_diff } - let(:outdated_position) { build_position(diff_refs: outdated_diff.diff_refs) } - let(:position) { build_position(diff_refs: latest_diff.diff_refs) } + context 'outdated position note' do + let!(:outdated_diff) { merge_request.merge_request_diff } + let!(:latest_diff) { merge_request.create_merge_request_diff } + let(:outdated_position) { build_position(diff_refs: outdated_diff.diff_refs) } + let(:position) { build_position(diff_refs: latest_diff.diff_refs) } - it 'uses the correct position when creating the suggestion' do - expect(note.position) - .to receive(:diff_file) - .with(project_with_repo.repository) - .and_call_original + it 'uses the correct position when creating the suggestion' do + expect(Gitlab::Diff::SuggestionsParser).to receive(:parse) + .with(note.note, project: note.project, position: note.position) + .and_call_original - subject.execute - end + subject.execute end end end diff --git a/spec/services/suggestions/outdate_service_spec.rb b/spec/services/suggestions/outdate_service_spec.rb new file mode 100644 index 00000000000..bcc627013d8 --- /dev/null +++ b/spec/services/suggestions/outdate_service_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Suggestions::OutdateService do + describe '#execute' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.target_project } + let(:user) { merge_request.author } + let(:file_path) { 'files/ruby/popen.rb' } + let(:branch_name) { project.default_branch } + let(:diff_file) { suggestion.diff_file } + let(:position) { build_position(file_path, comment_line) } + let(:note) do + create(:diff_note_on_merge_request, noteable: merge_request, + position: position, + project: project) + end + + def build_position(path, line) + Gitlab::Diff::Position.new(old_path: path, + new_path: path, + old_line: nil, + new_line: line, + diff_refs: merge_request.diff_refs) + end + + def commit_changes(file_path, new_content) + params = { + file_path: file_path, + commit_message: "Update File", + file_content: new_content, + start_project: project, + start_branch: project.default_branch, + branch_name: branch_name + } + + Files::UpdateService.new(project, user, params).execute + end + + def update_file_line(diff_file, change_line, content) + new_lines = diff_file.new_blob.data.lines + new_lines[change_line..change_line] = content + result = commit_changes(diff_file.file_path, new_lines.join) + newrev = result[:result] + + expect(result[:status]).to eq(:success) + expect(newrev).to be_present + + # Ensure all memoized data is cleared in order + # to generate the new merge_request_diff. + MergeRequest.find(merge_request.id).reload_diff(user) + + note.reload + end + + before do + project.add_maintainer(user) + end + + subject { described_class.new.execute(merge_request) } + + context 'when there is a change within multi-line suggestion range' do + let(:comment_line) { 9 } + let(:lines_above) { 8 } # suggesting to change lines 1..9 + let(:change_line) { 2 } # line 2 is within the range + let!(:suggestion) do + create(:suggestion, :content_from_repo, note: note, lines_above: lines_above) + end + + it 'updates the outdatable suggestion record' do + update_file_line(diff_file, change_line, "# foo\nbar\n") + + # Make sure note is still active + expect(note.active?).to be(true) + + expect { subject }.to change { suggestion.reload.outdated } + .from(false).to(true) + end + end + + context 'when there is no change within multi-line suggestion range' do + let(:comment_line) { 9 } + let(:lines_above) { 3 } # suggesting to change lines 6..9 + let(:change_line) { 2 } # line 2 is not within the range + let!(:suggestion) do + create(:suggestion, :content_from_repo, note: note, lines_above: lines_above) + end + + subject { described_class.new.execute(merge_request) } + + it 'does not outdates suggestion record' do + update_file_line(diff_file, change_line, "# foo\nbar\n") + + # Make sure note is still active + expect(note.active?).to be(true) + + expect { subject }.not_to change { suggestion.reload.outdated }.from(false) + end + end + end +end diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index d974cc0226f..ddf9d2b4917 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -9,88 +9,130 @@ describe VerifyPagesDomainService do subject(:service) { described_class.new(domain) } describe '#execute' do - context 'verification code recognition (verified domain)' do - where(:domain_sym, :code_sym) do - :domain | :verification_code - :domain | :keyed_verification_code + where(:domain_sym, :code_sym) do + :domain | :verification_code + :domain | :keyed_verification_code - :verification_domain | :verification_code - :verification_domain | :keyed_verification_code - end - - with_them do - set(:domain) { create(:pages_domain) } + :verification_domain | :verification_code + :verification_domain | :keyed_verification_code + end - let(:domain_name) { domain.send(domain_sym) } - let(:verification_code) { domain.send(code_sym) } + with_them do + let(:domain_name) { domain.send(domain_sym) } + let(:verification_code) { domain.send(code_sym) } + shared_examples 'verifies and enables the domain' do it 'verifies and enables the domain' do - stub_resolver(domain_name => ['something else', verification_code]) - expect(service.execute).to eq(status: :success) + expect(domain).to be_verified expect(domain).to be_enabled end + end - it 'verifies and enables when the code is contained partway through a TXT record' do - stub_resolver(domain_name => "something #{verification_code} else") + shared_examples 'successful enablement and verification' do + context 'when txt record contains verification code' do + before do + stub_resolver(domain_name => ['something else', verification_code]) + end - expect(service.execute).to eq(status: :success) - expect(domain).to be_verified - expect(domain).to be_enabled + include_examples 'verifies and enables the domain' end - it 'does not verify when the code is not present' do - stub_resolver(domain_name => 'something else') - - expect(service.execute).to eq(error_status) + context 'when txt record contains verification code with other text' do + before do + stub_resolver(domain_name => "something #{verification_code} else") + end - expect(domain).not_to be_verified - expect(domain).to be_enabled + include_examples 'verifies and enables the domain' end end - context 'verified domain' do - set(:domain) { create(:pages_domain) } + context 'when domain is disabled(or new)' do + let(:domain) { create(:pages_domain, :disabled) } - it 'unverifies (but does not disable) when the right code is not present' do - stub_resolver(domain.domain => 'something else') + include_examples 'successful enablement and verification' - expect(service.execute).to eq(error_status) - expect(domain).not_to be_verified - expect(domain).to be_enabled + shared_examples 'unverifies and disables domain' do + it 'unverifies and disables domain' do + expect(service.execute).to eq(error_status) + + expect(domain).not_to be_verified + expect(domain).not_to be_enabled + end end - it 'unverifies (but does not disable) when no records are present' do - stub_resolver + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') + end - expect(service.execute).to eq(error_status) - expect(domain).not_to be_verified - expect(domain).to be_enabled + include_examples 'unverifies and disables domain' + end + + context 'when no txt records are present' do + before do + stub_resolver + end + + include_examples 'unverifies and disables domain' end end - context 'expired domain' do - set(:domain) { create(:pages_domain, :expired) } + context 'when domain is verified' do + let(:domain) { create(:pages_domain) } - it 'verifies and enables when the right code is present' do - stub_resolver(domain.domain => domain.keyed_verification_code) + include_examples 'successful enablement and verification' - expect(service.execute).to eq(status: :success) + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') + end - expect(domain).to be_verified - expect(domain).to be_enabled + it 'unverifies but does not disable domain' do + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + expect(domain).to be_enabled + end end - it 'disables when the right code is not present' do - error_status[:message] += '. It is now disabled.' + context 'when no txt records are present' do + before do + stub_resolver + end - stub_resolver + it 'unverifies but does not disable domain' do + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + expect(domain).to be_enabled + end + end + end - expect(service.execute).to eq(error_status) + context 'when domain is expired' do + let(:domain) { create(:pages_domain, :expired) } - expect(domain).not_to be_verified - expect(domain).not_to be_enabled + context 'when the right code is present' do + before do + stub_resolver(domain_name => domain.keyed_verification_code) + end + + include_examples 'verifies and enables the domain' + end + + context 'when the right code is not present' do + before do + stub_resolver + end + + it 'disables domain' do + error_status[:message] += '. It is now disabled.' + + expect(service.execute).to eq(error_status) + + expect(domain).not_to be_verified + expect(domain).not_to be_enabled + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e8d7b18bf04..b3bd453ebd6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -96,6 +96,7 @@ RSpec.configure do |config| config.include MigrationsHelpers, :migration config.include RedisHelpers config.include Rails.application.routes.url_helpers, type: :routing + config.include PolicyHelpers, type: :policy if ENV['CI'] # This includes the first try, i.e. tests will be run 4 times before failing. diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index 5f709831ce1..63b719be03e 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -72,6 +72,15 @@ shared_examples_for 'group and project milestones' do |route_definition| expect(json_response.first['id']).to eq closed_milestone.id end + it 'returns a milestone by title' do + get api(route, user), params: { title: 'version2' } + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + it 'returns a milestone by searching for title' do get api(route, user), params: { search: 'version2' } diff --git a/spec/support/helpers/policy_helpers.rb b/spec/support/helpers/policy_helpers.rb new file mode 100644 index 00000000000..3d780eb5fb1 --- /dev/null +++ b/spec/support/helpers/policy_helpers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module PolicyHelpers + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end +end diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb new file mode 100644 index 00000000000..8bcd26ec0cd --- /dev/null +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.shared_context 'ProjectPolicy context' do + set(:guest) { create(:user) } + set(:reporter) { create(:user) } + set(:developer) { create(:user) } + set(:maintainer) { create(:user) } + set(:owner) { create(:user) } + set(:admin) { create(:admin) } + let(:project) { create(:project, :public, namespace: owner.namespace) } + + let(:base_guest_permissions) do + %i[ + read_project read_board read_list read_wiki read_issue + read_project_for_iids read_issue_iid read_label + read_milestone read_project_snippet read_project_member read_note + create_project create_issue create_note upload_file create_merge_request_in + award_emoji read_release + ] + end + + let(:base_reporter_permissions) do + %i[ + download_code fork_project create_project_snippet update_issue + admin_issue admin_label admin_list read_commit_status read_build + read_container_image read_pipeline read_environment read_deployment + read_merge_request download_wiki_code read_sentry_issue + ] + end + + let(:team_member_reporter_permissions) do + %i[build_download_code build_read_container_image] + end + + let(:developer_permissions) do + %i[ + admin_milestone admin_merge_request update_merge_request create_commit_status + update_commit_status create_build update_build create_pipeline + update_pipeline create_merge_request_from create_wiki push_code + resolve_note create_container_image update_container_image + create_environment create_deployment create_release update_release + ] + end + + let(:base_maintainer_permissions) do + %i[ + push_to_delete_protected_branch update_project_snippet update_environment + update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project + admin_commit_status admin_build admin_container_image + admin_pipeline admin_environment admin_deployment destroy_release add_cluster + daily_statistics + ] + end + + let(:public_permissions) do + %i[ + download_code fork_project read_commit_status read_pipeline + read_container_image build_download_code build_read_container_image + download_wiki_code read_release + ] + end + + let(:base_owner_permissions) do + %i[ + change_namespace change_visibility_level rename_project remove_project + archive_project remove_fork_project destroy_merge_request destroy_issue + set_issue_iid set_issue_created_at set_note_created_at + ] + end + + # Used in EE specs + let(:additional_guest_permissions) { [] } + let(:additional_reporter_permissions) { [] } + let(:additional_maintainer_permissions) { [] } + let(:additional_owner_permissions) { [] } + + let(:guest_permissions) { base_guest_permissions + additional_guest_permissions } + let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions } + let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions } + let(:owner_permissions) { base_owner_permissions + additional_owner_permissions } + + before do + project.add_guest(guest) + project.add_maintainer(maintainer) + project.add_developer(developer) + project.add_reporter(reporter) + end +end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb new file mode 100644 index 00000000000..b4808ac0068 --- /dev/null +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_context 'GroupPolicy context' do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:maintainer) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:group) { create(:group, :private) } + + let(:guest_permissions) do + %i[ + read_label read_group upload_file read_namespace read_group_activity + read_group_issues read_group_boards read_group_labels read_group_milestones + read_group_merge_requests + ] + end + let(:reporter_permissions) { [:admin_label] } + let(:developer_permissions) { [:admin_milestone] } + let(:maintainer_permissions) do + %i[ + create_projects + read_cluster create_cluster update_cluster admin_cluster add_cluster + ] + end + let(:owner_permissions) do + [ + :admin_group, + :admin_namespace, + :admin_group_member, + :change_visibility_level, + :set_note_created_at, + (Gitlab::Database.postgresql? ? :create_subgroup : nil) + ].compact + end + + before do + group.add_guest(guest) + group.add_reporter(reporter) + group.add_developer(developer) + group.add_maintainer(maintainer) + group.add_owner(owner) + end + + subject { described_class.new(current_user, group) } +end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb new file mode 100644 index 00000000000..7a71e2ee370 --- /dev/null +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'archived project policies' do + let(:feature_write_abilities) do + described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| + described_class.create_update_admin_destroy(feature) + end + additional_reporter_permissions + additional_maintainer_permissions + end + + let(:other_write_abilities) do + %i[ + create_merge_request_in + create_merge_request_from + push_to_delete_protected_branch + push_code + request_access + upload_file + resolve_note + award_emoji + ] + end + + context 'when the project is archived' do + before do + project.archived = true + end + + it 'disables write actions on all relevant project features' do + expect_disallowed(*feature_write_abilities) + end + + it 'disables some other important write actions' do + expect_disallowed(*other_write_abilities) + end + + it 'does not disable other abilities' do + expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities)) + end + end +end + +RSpec.shared_examples 'project policies as anonymous' do + context 'abilities for public projects' do + context 'when a project has pending invites' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:anonymous_permissions) { guest_permissions - user_permissions } + + subject { described_class.new(nil, project) } + + before do + create(:group_member, :invited, group: group) + end + + it 'does not grant owner access' do + expect_allowed(*anonymous_permissions) + expect_disallowed(*user_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { anonymous_permissions } + end + end + end + + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(nil, project) } + + it { is_expected.to be_banned } + end +end + +RSpec.shared_examples 'project policies as guest' do + subject { described_class.new(guest, project) } + + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + let(:reporter_public_build_permissions) do + reporter_permissions - [:read_build, :read_pipeline] + end + + it do + expect_allowed(*guest_permissions) + expect_disallowed(*reporter_public_build_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { guest_permissions } + end + + context 'public builds enabled' do + it do + expect_allowed(*guest_permissions) + expect_allowed(:read_build, :read_pipeline) + end + end + + context 'when public builds disabled' do + before do + project.update(public_builds: false) + end + + it do + expect_allowed(*guest_permissions) + expect_disallowed(:read_build, :read_pipeline) + end + end + + context 'when builds are disabled' do + before do + project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) + end + + it do + expect_disallowed(:read_build) + expect_allowed(:read_pipeline) + end + end + end +end + +RSpec.shared_examples 'project policies as reporter' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(reporter, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { reporter_permissions } + end + end +end + +RSpec.shared_examples 'project policies as developer' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + subject { described_class.new(developer, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { developer_permissions } + end + end +end + +RSpec.shared_examples 'project policies as maintainer' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(maintainer, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { maintainer_permissions } + end + end +end + +RSpec.shared_examples 'project policies as owner' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(owner, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*maintainer_permissions) + expect_allowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { owner_permissions } + end + end +end + +RSpec.shared_examples 'project policies as admin' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(admin, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*maintainer_permissions) + expect_allowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { owner_permissions } + end + end +end diff --git a/spec/workers/migrate_external_diffs_worker_spec.rb b/spec/workers/migrate_external_diffs_worker_spec.rb new file mode 100644 index 00000000000..88d48cad14b --- /dev/null +++ b/spec/workers/migrate_external_diffs_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MigrateExternalDiffsWorker do + let(:worker) { described_class.new } + let(:diff) { create(:merge_request).merge_request_diff } + + describe '#perform' do + it 'migrates the listed diff' do + expect_next_instance_of(MergeRequests::MigrateExternalDiffsService) do |instance| + expect(instance.diff).to eq(diff) + expect(instance).to receive(:execute) + end + + worker.perform(diff.id) + end + + it 'does nothing if the diff is missing' do + diff.destroy + + worker.perform(diff.id) + end + end +end diff --git a/spec/workers/schedule_migrate_external_diffs_worker_spec.rb b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb new file mode 100644 index 00000000000..9d6fecc9f4e --- /dev/null +++ b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ScheduleMigrateExternalDiffsWorker do + include ExclusiveLeaseHelpers + + let(:worker) { described_class.new } + + describe '#perform' do + it 'triggers a scan for diffs to migrate' do + expect(MergeRequests::MigrateExternalDiffsService).to receive(:enqueue!) + + worker.perform + end + + it 'will not run if the lease is already taken' do + stub_exclusive_lease_taken('schedule_migrate_external_diffs_worker', timeout: 2.hours) + + expect(MergeRequests::MigrateExternalDiffsService).not_to receive(:enqueue!) + + worker.perform + end + end +end diff --git a/yarn.lock b/yarn.lock index 4a7443c0bd8..2bdbca103b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -638,12 +638,12 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" -"@gitlab/csslab@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@gitlab/csslab/-/csslab-1.8.0.tgz#54a2457fdc80f006665f0e578a5532780954ccfa" - integrity sha512-RZylRElufH1kwsBQlIDaVcrcXMyD5IEGrU6ABUd8W3LG8/F9jJ4Y3Ys7EPTpK/qFJyx86AutTtFGRxRNlMx85w== +"@gitlab/csslab@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@gitlab/csslab/-/csslab-1.9.0.tgz#22fca5b1a30cbd9ca46fc6f9485ecbaba4dc300c" + integrity sha512-Zjayzokm7E2wgxUR/pxIMocdiBB5XHt2PEemdzD8qD+aQmMpMxSyIEMQk5Jq0Wgv+Rd5WXTolTw3kmb9l8ZeJg== dependencies: - bootstrap "4.1.3" + bootstrap "^4.1.3" "@gitlab/eslint-config@^1.4.0": version "1.4.0" @@ -707,10 +707,10 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== -"@types/glob@^5": - version "5.0.35" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a" - integrity sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg== +"@types/glob@5 - 7": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== dependencies: "@types/events" "*" "@types/minimatch" "*" @@ -791,7 +791,7 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@vue/component-compiler-utils@^2.0.0", "@vue/component-compiler-utils@^2.4.0": +"@vue/component-compiler-utils@^2.4.0", "@vue/component-compiler-utils@^2.5.1": version "2.6.0" resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz#aa46d2a6f7647440b0b8932434d22f12371e543b" integrity sha512-IHjxt7LsOFYc0DkTncB7OXJL7UzwOLPPQCfEUNyxL2qt+tF12THV+EO33O1G2Uk4feMSWua3iD39Itszx0f0bw== @@ -1681,10 +1681,10 @@ bootstrap-vue@^2.0.0-rc.11: popper.js "^1.12.9" vue-functional-data-merge "^2.0.5" -bootstrap@4.1.3, bootstrap@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be" - integrity sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w== +bootstrap@4.3.1, bootstrap@^4.1.1, bootstrap@^4.1.3: + version "4.3.1" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" + integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== boxen@^1.2.1: version "1.3.0" @@ -4546,27 +4546,27 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -gettext-extractor-vue@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-4.0.1.tgz#69d2737eb8f1938803ffcf9317133ed59fb2372f" - integrity sha512-UnkWVO5jQQrs17L7HSlKj3O7U8C4+AQFzE05MK/I+JkMZdQdB6JMjA0IK0c4GObSlkgx4aiCCG6zWqIBnDR95w== +gettext-extractor-vue@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-4.0.2.tgz#16e1cdbdaf37e5bdf3cb0aff63685bdc5e74e906" + integrity sha512-tnTAU1TdQFREv4Q4hfBDuB329eugeFsYmV7lE9U1jkZEyxcf4oPgimLHNZVNaEUg4+JJwhB8B9HIeqbcbSW32g== dependencies: bluebird "^3.5.1" glob "^7.1.2" - vue-template-compiler "^2.5.0" + vue-template-compiler "^2.5.20" -gettext-extractor@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.3.2.tgz#d5172ba8d175678bd40a5abe7f908fa2a9d9473b" - integrity sha1-1RcrqNF1Z4vUClq+f5CPoqnZRzs= +gettext-extractor@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.4.3.tgz#882679cefc71888eb6e69297e6b2dc14c0384fef" + integrity sha512-YSNdTCHmzm58Rc21thtXj7jRIOlqINftM3XbtvNK28C88i35EnEB89iOeV9Vetv7wcb/wiPAtcq/6iSnt2pMyw== dependencies: - "@types/glob" "^5" + "@types/glob" "5 - 7" "@types/parse5" "^5" css-selector-parser "^1.3" glob "5 - 7" parse5 "^5" pofile "^1" - typescript "^2" + typescript "2 - 3" glob-parent@^3.1.0: version "3.1.0" @@ -8121,10 +8121,10 @@ pofile@^1: resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954" integrity sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg== -popper.js@^1.12.9, popper.js@^1.14.3: - version "1.14.3" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" - integrity sha1-FDj5jQRqz3tNeM1QK/QYrGTU8JU= +popper.js@^1.12.9, popper.js@^1.14.7: + version "1.14.7" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" + integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ== portfinder@^1.0.9: version "1.0.13" @@ -10512,10 +10512,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" - integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== +"typescript@2 - 3": + version "3.3.4000" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.4000.tgz#76b0f89cfdbf97827e1112d64f283f1151d6adf0" + integrity sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" @@ -10938,12 +10938,12 @@ vue-jest@^4.0.0-beta.2: source-map "^0.5.6" ts-jest "^23.10.5" -vue-loader@^15.4.2: - version "15.4.2" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.4.2.tgz#812bb26e447dd3b84c485eb634190d914ce125e2" - integrity sha512-nVV27GNIA9MeoD8yQ3dkUzwlAaAsWeYSWZHsu/K04KCD339lW0Jv2sJWsjj3721SP7sl2lYdPmjcHgkWQSp5bg== +vue-loader@^15.4.2, vue-loader@^15.7.0: + version "15.7.0" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.0.tgz#27275aa5a3ef4958c5379c006dd1436ad04b25b3" + integrity sha512-x+NZ4RIthQOxcFclEcs8sXGEWqnZHodL2J9Vq+hUz+TDZzBaDIh1j3d9M2IUlTjtrHTZy4uMuRdTi8BGws7jLA== dependencies: - "@vue/component-compiler-utils" "^2.0.0" + "@vue/component-compiler-utils" "^2.5.1" hash-sum "^1.0.2" loader-utils "^1.1.0" vue-hot-reload-api "^2.3.0" @@ -10969,10 +10969,10 @@ vue-style-loader@^4.1.0: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-template-compiler@^2.5.0, vue-template-compiler@^2.5.21: - version "2.5.21" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a" - integrity sha512-Vmk5Cv7UcmI99B9nXJEkaK262IQNnHp5rJYo+EwYpe2epTAXqcVyExhV6pk8jTkxQK2vRc8v8KmZBAwdmUZvvw== +vue-template-compiler@^2.5.20, vue-template-compiler@^2.6.10: + version "2.6.10" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc" + integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg== dependencies: de-indent "^1.0.2" he "^1.1.0" @@ -10982,20 +10982,20 @@ vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== -vue-virtual-scroll-list@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a" - integrity sha1-vL0BD3zbA166iVjr+AfGIU2aFno= +vue-virtual-scroll-list@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76" + integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g== -vue@^2.5.21: - version "2.5.21" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85" - integrity sha512-Aejvyyfhn0zjVeLvXd70h4hrE4zZDx1wfZqia6ekkobLmUZ+vNFQer53B4fu0EjWBSiqApxPejzkO1Znt3joxQ== +vue@^2.5.21, vue@^2.6.10: + version "2.6.10" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637" + integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ== -vuex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" - integrity sha512-wLoqz0B7DSZtgbWL1ShIBBCjv22GV5U+vcBFox658g6V0s4wZV9P4YjCNyoHSyIBpj1f29JBoNQIqD82cR4O3w== +vuex@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.0.tgz#634b81515cf0cfe976bd1ffe9601755e51f843b9" + integrity sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg== w3c-hr-time@^1.0.1: version "1.0.1" |