diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-13 21:08:59 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-13 21:08:59 +0000 |
commit | d466ee5042520ad078fe050cb078d81dc2ebe196 (patch) | |
tree | 5648eb1aee8aeff5b5c5ff4669a184fd7676f778 | |
parent | 6a9d7c009e4e5975a89bcc3e458da4b3ec484bd1 (diff) | |
download | gitlab-ce-d466ee5042520ad078fe050cb078d81dc2ebe196.tar.gz |
Add latest changes from gitlab-org/gitlab@master
91 files changed, 1483 insertions, 728 deletions
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md index 3e09b0ef702..f1224820fe4 100644 --- a/CHANGELOG-EE.md +++ b/CHANGELOG-EE.md @@ -106,6 +106,10 @@ Please view this file on the master branch, on stable branches it's out of date. - Remove "creations" in gitlab_subscription_histories on gitlab.com. !22278 +## 12.6.7 + +- No changes. + ## 12.6.6 - No changes. @@ -455,7 +455,7 @@ group :ed25519 do end # Gitaly GRPC protocol definitions -gem 'gitaly', '~> 1.85.0' +gem 'gitaly', '~> 1.86.0' gem 'grpc', '~> 1.24.0' diff --git a/Gemfile.lock b/Gemfile.lock index 276f9da13c7..817648c606f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -375,7 +375,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) git (1.5.0) - gitaly (1.85.0) + gitaly (1.86.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-chronic (0.10.5) @@ -1230,7 +1230,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly (~> 1.85.0) + gitaly (~> 1.86.0) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) gitlab-labkit (= 0.9.1) diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue index 500f6737839..d36adbd798e 100644 --- a/app/assets/javascripts/ide/components/error_message.vue +++ b/app/assets/javascripts/ide/components/error_message.vue @@ -1,9 +1,10 @@ <script> import { mapActions } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; export default { components: { + GlAlert, GlLoadingIcon, }, props: { @@ -17,9 +18,14 @@ export default { isLoading: false, }; }, + computed: { + canDismiss() { + return !this.message.action; + }, + }, methods: { ...mapActions(['setErrorMessage']), - clickAction() { + doAction() { if (this.isLoading) return; this.isLoading = true; @@ -33,28 +39,23 @@ export default { this.isLoading = false; }); }, - clickFlash() { - if (!this.message.action) { - this.setErrorMessage(null); - } + dismiss() { + this.setErrorMessage(null); }, }, }; </script> <template> - <div class="flash-container flash-container-page" @click="clickFlash"> - <div class="flash-alert" data-qa-selector="flash_alert"> - <span v-html="message.text"> </span> - <button - v-if="message.action" - type="button" - class="flash-action text-white p-0 border-top-0 border-right-0 border-left-0 bg-transparent" - @click.stop.prevent="clickAction" - > - {{ message.actionText }} - <gl-loading-icon v-show="isLoading" inline /> - </button> - </div> - </div> + <gl-alert + data-qa-selector="flash_alert" + variant="danger" + :dismissible="canDismiss" + :primary-button-text="message.actionText" + @dismiss="dismiss" + @primaryAction="doAction" + > + <span v-html="message.text"></span> + <gl-loading-icon v-show="isLoading" inline class="vertical-align-middle ml-1" /> + </gl-alert> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 4dc6e51d2fc..6a836adba01 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,5 +1,4 @@ <script> -import _ from 'underscore'; import { GlLoadingIcon } from '@gitlab/ui'; import StageColumnComponent from './stage_column_component.vue'; import GraphMixin from '../../mixins/graph_component_mixin'; @@ -70,7 +69,7 @@ export default { expandedTriggeredBy() { return ( this.pipeline.triggered_by && - _.isArray(this.pipeline.triggered_by) && + Array.isArray(this.pipeline.triggered_by) && this.pipeline.triggered_by.find(el => el.isExpanded) ); }, diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index db7714808fd..3d3dabbdf22 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty, escape as esc } from 'lodash'; import stageColumnMixin from '../../mixins/stage_column_mixin'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; @@ -39,12 +39,12 @@ export default { }, computed: { hasAction() { - return !_.isEmpty(this.action); + return !isEmpty(this.action); }, }, methods: { groupId(group) { - return `ci-badge-${_.escape(group.name)}`; + return `ci-badge-${esc(group.name)}`; }, pipelineActionRequestComplete() { this.$emit('refreshPipelineGraph'); diff --git a/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue index 6ca96bbba5e..f604edd8859 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_stop_modal.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import { GlLink } from '@gitlab/ui'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -43,7 +43,7 @@ export default { ); }, hasRef() { - return !_.isEmpty(this.pipeline.ref); + return !isEmpty(this.pipeline.ref); }, }, methods: { diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 743c3ea271d..0c9d242f509 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,11 +1,11 @@ <script> import { GlLink, GlTooltipDirective } from '@gitlab/ui'; -import _ from 'underscore'; +import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; import popover from '~/vue_shared/directives/popover'; const popoverTitle = sprintf( - _.escape( + escape( __( `This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}`, ), @@ -49,7 +49,7 @@ export default { href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow"> - ${_.escape(__('Learn more about Auto DevOps'))} + ${escape(__('Learn more about Auto DevOps'))} </a>`, }; }, diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index d730ef41b1a..accd6bf71f4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEqual } from 'lodash'; import { __, sprintf, s__ } from '../../locale'; import createFlash from '../../flash'; import PipelinesService from '../services/pipelines_service'; @@ -218,7 +218,7 @@ export default { successCallback(resp) { // Because we are polling & the user is interacting verify if the response received // matches the last request made - if (_.isEqual(resp.config.params, this.requestData)) { + if (isEqual(resp.config.params, this.requestData)) { this.store.storeCount(resp.data.count); this.store.storePagination(resp.headers); this.setCommonData(resp.data.pipelines); diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js index f383a4b3368..53b7a174517 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { escape } from 'lodash'; export default { props: { @@ -18,7 +18,7 @@ export default { }, methods: { capitalizeStageName(name) { - const escapedName = _.escape(name); + const escapedName = escape(name); return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); }, isFirstColumn(index) { diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 441c9f3c25f..69e3579a3c7 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import _ from 'underscore'; export default class PipelineStore { constructor() { @@ -61,7 +60,7 @@ export default class PipelineStore { Vue.set(newPipeline, 'isLoading', false); if (newPipeline.triggered_by) { - if (!_.isArray(newPipeline.triggered_by)) { + if (!Array.isArray(newPipeline.triggered_by)) { Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] }); } this.parseTriggeredByPipelines(oldPipeline, newPipeline.triggered_by[0]); diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue index d542dad8119..2ac57ac5bcb 100644 --- a/app/assets/javascripts/serverless/components/function_details.vue +++ b/app/assets/javascripts/serverless/components/function_details.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isString } from 'lodash'; import { mapState, mapActions, mapGetters } from 'vuex'; import PodBox from './pod_box.vue'; import Url from './url.vue'; @@ -42,7 +42,7 @@ export default { return this.func.name; }, description() { - return _.isString(this.func.description) ? this.func.description : ''; + return isString(this.func.description) ? this.func.description : ''; }, funcUrl() { return this.func.url; diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue index dca0e01b250..bbafdd7f8f1 100644 --- a/app/assets/javascripts/serverless/components/function_row.vue +++ b/app/assets/javascripts/serverless/components/function_row.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isString } from 'lodash'; import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; import Url from './url.vue'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -20,7 +20,7 @@ export default { return this.func.name; }, description() { - if (!_.isString(this.func.description)) { + if (!isString(this.func.description)) { return ''; } diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue index 932ca8e002e..9ac687f5e2c 100644 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue @@ -5,6 +5,7 @@ import { __, sprintf } from '~/locale'; import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range'; import Icon from '~/vue_shared/components/icon.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import DateTimePickerInput from './date_time_picker_input.vue'; import { defaultTimeRanges, @@ -24,6 +25,7 @@ const events = { export default { components: { Icon, + TooltipOnTruncate, DateTimePickerInput, GlFormGroup, GlButton, @@ -149,61 +151,68 @@ export default { }; </script> <template> - <gl-dropdown - :text="timeWindowText" - class="date-time-picker" - menu-class="date-time-picker-menu" - v-bind="$attrs" - toggle-class="w-100 text-truncate" + <tooltip-on-truncate + :title="timeWindowText" + :truncate-target="elem => elem.querySelector('.date-time-picker-toggle')" + placement="top" + class="d-inline-block" > - <div class="d-flex justify-content-between gl-p-2"> - <gl-form-group - :label="__('Custom range')" - label-for="custom-from-time" - label-class="gl-pb-1" - class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0" - > - <div class="gl-pt-2"> - <date-time-picker-input - id="custom-time-from" - v-model="startInput" - :label="__('From')" - :state="startInputValid" - /> - <date-time-picker-input - id="custom-time-to" - v-model="endInput" - :label="__('To')" - :state="endInputValid" - /> - </div> - <gl-form-group> - <gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button> - <gl-button variant="success" :disabled="!isValid" @click="setFixedRange()"> - {{ __('Apply') }} - </gl-button> + <gl-dropdown + :text="timeWindowText" + v-bind="$attrs" + class="date-time-picker w-100" + menu-class="date-time-picker-menu" + toggle-class="date-time-picker-toggle text-truncate" + > + <div class="d-flex justify-content-between gl-p-2"> + <gl-form-group + :label="__('Custom range')" + label-for="custom-from-time" + label-class="gl-pb-1" + class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0" + > + <div class="gl-pt-2"> + <date-time-picker-input + id="custom-time-from" + v-model="startInput" + :label="__('From')" + :state="startInputValid" + /> + <date-time-picker-input + id="custom-time-to" + v-model="endInput" + :label="__('To')" + :state="endInputValid" + /> + </div> + <gl-form-group> + <gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button> + <gl-button variant="success" :disabled="!isValid" @click="setFixedRange()"> + {{ __('Apply') }} + </gl-button> + </gl-form-group> </gl-form-group> - </gl-form-group> - <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0"> - <template #label> - <span class="gl-pl-5">{{ __('Quick range') }}</span> - </template> + <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0"> + <template #label> + <span class="gl-pl-5">{{ __('Quick range') }}</span> + </template> - <gl-dropdown-item - v-for="(option, index) in options" - :key="index" - :active="isOptionActive(option)" - active-class="active" - @click="setQuickRange(option)" - > - <icon - name="mobile-issue-close" - class="align-bottom" - :class="{ invisible: !isOptionActive(option) }" - /> - {{ option.label }} - </gl-dropdown-item> - </gl-form-group> - </div> - </gl-dropdown> + <gl-dropdown-item + v-for="(option, index) in options" + :key="index" + :active="isOptionActive(option)" + active-class="active" + @click="setQuickRange(option)" + > + <icon + name="mobile-issue-close" + class="align-bottom" + :class="{ invisible: !isOptionActive(option) }" + /> + {{ option.label }} + </gl-dropdown-item> + </gl-form-group> + </div> + </gl-dropdown> + </tooltip-on-truncate> </template> diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue index 69eb791d195..4ea3d162da2 100644 --- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue +++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isFunction } from 'lodash'; import tooltip from '../directives/tooltip'; export default { @@ -28,16 +28,18 @@ export default { showTooltip: false, }; }, + watch: { + title() { + // Wait on $nextTick in case of slot width changes + this.$nextTick(this.updateTooltip); + }, + }, mounted() { - const target = this.selectTarget(); - - if (target && target.scrollWidth > target.offsetWidth) { - this.showTooltip = true; - } + this.updateTooltip(); }, methods: { selectTarget() { - if (_.isFunction(this.truncateTarget)) { + if (isFunction(this.truncateTarget)) { return this.truncateTarget(this.$el); } else if (this.truncateTarget === 'child') { return this.$el.childNodes[0]; @@ -45,6 +47,10 @@ export default { return this.$el; }, + updateTooltip() { + const target = this.selectTarget(); + this.showTooltip = Boolean(target && target.scrollWidth > target.offsetWidth); + }, }, }; </script> diff --git a/app/models/project.rb b/app/models/project.rb index bc652a19986..a215b6c881c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -901,7 +901,9 @@ class Project < ApplicationRecord if Gitlab::UrlSanitizer.valid?(value) import_url = Gitlab::UrlSanitizer.new(value) super(import_url.sanitized_url) - create_or_update_import_data(credentials: import_url.credentials) + + credentials = import_url.credentials.to_h.transform_values { |value| CGI.unescape(value.to_s) } + create_or_update_import_data(credentials: credentials) else super(value) end diff --git a/app/models/repository.rb b/app/models/repository.rb index c439d0700f1..37aceeae5f8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -134,15 +134,6 @@ class Repository end end - # the opts are: - # - :path - # - :limit - # - :offset - # - :skip_merges - # - :after - # - :before - # - :all - # - :first_parent def commits(ref = nil, opts = {}) options = { repo: raw_repository, @@ -155,7 +146,8 @@ class Repository after: opts[:after], before: opts[:before], all: !!opts[:all], - first_parent: !!opts[:first_parent] + first_parent: !!opts[:first_parent], + order: opts[:order] } commits = Gitlab::Git::Commit.where(options) diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index ae67b4f5256..0e7a4821bdf 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -19,8 +19,10 @@ module Users LEASE_TIMEOUT = 1.minute.to_i # user - The User for which to refresh the authorized projects. - def initialize(user) + def initialize(user, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil) @user = user + @incorrect_auth_found_callback = incorrect_auth_found_callback + @missing_auth_found_callback = missing_auth_found_callback # We need an up to date User object that has access to all relations that # may have been created earlier. The only way to ensure this is to reload @@ -55,6 +57,10 @@ module Users # rows not in the new list or with a different access level should be # removed. if !fresh[project_id] || fresh[project_id] != row.access_level + if incorrect_auth_found_callback + incorrect_auth_found_callback.call(project_id, row.access_level) + end + array << row.project_id end end @@ -63,6 +69,10 @@ module Users # rows not in the old list or with a different access level should be # added. if !current[project_id] || current[project_id].access_level != level + if missing_auth_found_callback + missing_auth_found_callback.call(project_id, level) + end + array << [user.id, project_id, level] end end @@ -104,5 +114,9 @@ module Users def fresh_authorizations Gitlab::ProjectAuthorizations.new(user).calculate end + + private + + attr_reader :incorrect_auth_found_callback, :missing_auth_found_callback end end diff --git a/changelogs/unreleased/198460-date-time-picker-custom-dates-make-the-date-field-too-long-truncat.yml b/changelogs/unreleased/198460-date-time-picker-custom-dates-make-the-date-field-too-long-truncat.yml new file mode 100644 index 00000000000..93d1f7537fd --- /dev/null +++ b/changelogs/unreleased/198460-date-time-picker-custom-dates-make-the-date-field-too-long-truncat.yml @@ -0,0 +1,5 @@ +--- +title: Add tooltip when dates in date picker are too long +merge_request: 24664 +author: +type: added diff --git a/changelogs/unreleased/35671-api-repository-commits-with-order.yml b/changelogs/unreleased/35671-api-repository-commits-with-order.yml new file mode 100644 index 00000000000..3ca59a2265f --- /dev/null +++ b/changelogs/unreleased/35671-api-repository-commits-with-order.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Ability to list commits in order (--topo-order)' +merge_request: 24702 +author: +type: added diff --git a/changelogs/unreleased/36854-webide-use-gl-alert.yml b/changelogs/unreleased/36854-webide-use-gl-alert.yml new file mode 100644 index 00000000000..4f6cc63de3f --- /dev/null +++ b/changelogs/unreleased/36854-webide-use-gl-alert.yml @@ -0,0 +1,5 @@ +--- +title: Fix Web IDE alert message look and feel +merge_request: 23300 +author: Sean Nichols +type: fixed diff --git a/changelogs/unreleased/avoid-double-credential-encoding-on-project-importing.yml b/changelogs/unreleased/avoid-double-credential-encoding-on-project-importing.yml new file mode 100644 index 00000000000..5118be38704 --- /dev/null +++ b/changelogs/unreleased/avoid-double-credential-encoding-on-project-importing.yml @@ -0,0 +1,5 @@ +--- +title: Avoid double encoding of credential while importing a Project by URL +merge_request: 24514 +author: +type: fixed diff --git a/changelogs/unreleased/leipert-fix-user-label-bug.yml b/changelogs/unreleased/leipert-fix-user-label-bug.yml deleted file mode 100644 index 67e1a4011af..00000000000 --- a/changelogs/unreleased/leipert-fix-user-label-bug.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix autocomplete limitation bug -merge_request: 25127 -author: -type: fixed diff --git a/changelogs/unreleased/refactoring-entities-file-15.yml b/changelogs/unreleased/refactoring-entities-file-15.yml new file mode 100644 index 00000000000..89c2da2cf86 --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-15.yml @@ -0,0 +1,5 @@ +--- +title: Separate access entities into own class files +merge_request: 24845 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-23.yml b/changelogs/unreleased/refactoring-entities-file-23.yml new file mode 100644 index 00000000000..c771b80811f --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-23.yml @@ -0,0 +1,5 @@ +--- +title: Separate environment entities into own class files +merge_request: 24951 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-26.yml b/changelogs/unreleased/refactoring-entities-file-26.yml new file mode 100644 index 00000000000..050ab6678a5 --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-26.yml @@ -0,0 +1,5 @@ +--- +title: Separate JobRequest entities into own class files +merge_request: 24977 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-28.yml b/changelogs/unreleased/refactoring-entities-file-28.yml new file mode 100644 index 00000000000..604dfa7627d --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-28.yml @@ -0,0 +1,5 @@ +--- +title: Separate page domain entities into own class files +merge_request: 24987 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-30.yml b/changelogs/unreleased/refactoring-entities-file-30.yml new file mode 100644 index 00000000000..c60c63b9843 --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-30.yml @@ -0,0 +1,5 @@ +--- +title: Separate badge entities into own class files +merge_request: 25116 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-32.yml b/changelogs/unreleased/refactoring-entities-file-32.yml new file mode 100644 index 00000000000..e5116a80263 --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-32.yml @@ -0,0 +1,5 @@ +--- +title: Separate cluster entities into own class files +merge_request: 25121 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/resolve_gitlab_issue_196651.yml b/changelogs/unreleased/resolve_gitlab_issue_196651.yml new file mode 100644 index 00000000000..169d8e50383 --- /dev/null +++ b/changelogs/unreleased/resolve_gitlab_issue_196651.yml @@ -0,0 +1,5 @@ +--- +title: Replace underscore with lodash for ./app/assets/javascripts/serverless +merge_request: 25011 +author: Tobias Spagert +type: other diff --git a/db/post_migrate/20200204113223_schedule_recalculate_project_authorizations.rb b/db/post_migrate/20200204113223_schedule_recalculate_project_authorizations.rb new file mode 100644 index 00000000000..83b58300115 --- /dev/null +++ b/db/post_migrate/20200204113223_schedule_recalculate_project_authorizations.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class ScheduleRecalculateProjectAuthorizations < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'RecalculateProjectAuthorizations' + BATCH_SIZE = 2_500 + DELAY_INTERVAL = 2.minutes.to_i + + disable_ddl_transaction! + + class Namespace < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'namespaces' + end + + class ProjectAuthorization < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'project_authorizations' + end + + def up + say "Scheduling #{MIGRATION} jobs" + + max_group_id = Namespace.where(type: 'Group').maximum(:id) + project_authorizations = ProjectAuthorization.where('project_id <= ?', max_group_id) + .select(:user_id) + .distinct + + project_authorizations.each_batch(of: BATCH_SIZE, column: :user_id) do |authorizations, index| + delay = index * DELAY_INTERVAL + user_ids = authorizations.map(&:user_id) + BackgroundMigrationWorker.perform_in(delay, MIGRATION, [user_ids]) + end + end + + def down + end +end diff --git a/doc/api/commits.md b/doc/api/commits.md index fb090f51a2e..eb3fb7b2195 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -18,6 +18,7 @@ GET /projects/:id/repository/commits | `all` | boolean | no | Retrieve every commit from the repository | | `with_stats` | boolean | no | Stats about each commit will be added to the response | | `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit | +| `order` | string | no | List commits in order. Possible value: [`topo`](https://git-scm.com/docs/git-log#Documentation/git-log.txt---topo-order). | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits" diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md index e87b5d03438..e7565835be7 100644 --- a/doc/user/project/operations/error_tracking.md +++ b/doc/user/project/operations/error_tracking.md @@ -84,5 +84,6 @@ Ignoring an error will prevent it from appearing in the [Error Tracking List](#e From within the [Error Details](#error-details) page you can resolve a Sentry error by clicking the **Resolve** button near the top of the page. -Marking an error as resolved indicates that the error has stopped firing events. If another event -occurs, the error reverts to unresolved. +Marking an error as resolved indicates that the error has stopped firing events. If a GitLab issue is linked to the error, then the issue will be closed. + +If another event occurs, the error reverts to unresolved. diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9dcf9b015aa..4e04a99e97c 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -38,6 +38,7 @@ module API optional :all, type: Boolean, desc: 'Every commit will be returned' optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges' + optional :order, type: String, desc: 'List commits in order', values: %w[topo] use :pagination end get ':id/repository/commits' do @@ -49,6 +50,7 @@ module API all = params[:all] with_stats = params[:with_stats] first_parent = params[:first_parent] + order = params[:order] commits = user_project.repository.commits(ref, path: path, @@ -57,7 +59,8 @@ module API before: before, after: after, all: all, - first_parent: first_parent) + first_parent: first_parent, + order: order) commit_count = if all || path || before || after || first_parent diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 5128ffa6a1f..b9805973c54 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -129,40 +129,6 @@ module API end end - class Namespace < NamespaceBasic - expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _| - namespace.users_with_descendants.count - end - - def expose_members_count_with_descendants?(namespace, opts) - namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace) - end - end - - class MemberAccess < Grape::Entity - expose :access_level - expose :notification_level do |member, options| - if member.notification_setting - ::NotificationSetting.levels[member.notification_setting.level] - end - end - end - - class ProjectAccess < MemberAccess - end - - class GroupAccess < MemberAccess - end - - class NotificationSetting < Grape::Entity - expose :level - expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do - ::NotificationSetting.email_events.each do |event| - expose event - end - end - end - class Trigger < Grape::Entity include ::API::Helpers::Presentable @@ -204,39 +170,6 @@ module API expose :variables, using: Entities::Variable end - class EnvironmentBasic < Grape::Entity - expose :id, :name, :slug, :external_url - end - - class Deployment < Grape::Entity - expose :id, :iid, :ref, :sha, :created_at, :updated_at - expose :user, using: Entities::UserBasic - expose :environment, using: Entities::EnvironmentBasic - expose :deployable, using: Entities::Job - expose :status - end - - class Environment < EnvironmentBasic - expose :project, using: Entities::BasicProjectDetails - expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } - expose :state - end - - class LicenseBasic < Grape::Entity - expose :key, :name, :nickname - expose :url, as: :html_url - expose(:source_url) { |license| license.meta['source'] } - end - - class License < LicenseBasic - expose :popular?, as: :popular - expose(:description) { |license| license.meta['description'] } - expose(:conditions) { |license| license.meta['conditions'] } - expose(:permissions) { |license| license.meta['permissions'] } - expose(:limitations) { |license| license.meta['limitations'] } - expose :content - end - class ImpersonationToken < PersonalAccessToken expose :impersonation end @@ -267,93 +200,6 @@ module API end end - module JobRequest - class JobInfo < Grape::Entity - expose :name, :stage - expose :project_id, :project_name - end - - class GitInfo < Grape::Entity - expose :repo_url, :ref, :sha, :before_sha - expose :ref_type - expose :refspecs - expose :git_depth, as: :depth - end - - class RunnerInfo < Grape::Entity - expose :metadata_timeout, as: :timeout - expose :runner_session_url - end - - class Step < Grape::Entity - expose :name, :script, :timeout, :when, :allow_failure - end - - class Port < Grape::Entity - expose :number, :protocol, :name - end - - class Image < Grape::Entity - expose :name, :entrypoint - expose :ports, using: JobRequest::Port - end - - class Service < Image - expose :alias, :command - end - - class Artifacts < Grape::Entity - expose :name - expose :untracked - expose :paths - expose :when - expose :expire_in - expose :artifact_type - expose :artifact_format - end - - class Cache < Grape::Entity - expose :key, :untracked, :paths, :policy - end - - class Credentials < Grape::Entity - expose :type, :url, :username, :password - end - - class Dependency < Grape::Entity - expose :id, :name, :token - expose :artifacts_file, using: JobArtifactFile, if: ->(job, _) { job.artifacts? } - end - - class Response < Grape::Entity - expose :id - expose :token - expose :allow_git_fetch - - expose :job_info, using: JobInfo do |model| - model - end - - expose :git_info, using: GitInfo do |model| - model - end - - expose :runner_info, using: RunnerInfo do |model| - model - end - - expose :variables - expose :steps, using: Step - expose :image, using: Image - expose :services, using: Service - expose :artifacts, using: Artifacts - expose :cache, using: Cache - expose :credentials, using: Credentials - expose :all_dependencies, as: :dependencies, using: Dependency - expose :features - end - end - class UserAgentDetail < Grape::Entity expose :user_agent expose :ip_address @@ -370,45 +216,6 @@ module API expose :expiration end - class PagesDomainCertificate < Grape::Entity - expose :subject - expose :expired?, as: :expired - expose :certificate - expose :certificate_text - end - - class PagesDomainBasic < Grape::Entity - expose :domain - expose :url - expose :project_id - expose :verified?, as: :verified - expose :verification_code, as: :verification_code - expose :enabled_until - expose :auto_ssl_enabled - - expose :certificate, - as: :certificate_expiration, - if: ->(pages_domain, _) { pages_domain.certificate? }, - using: PagesDomainCertificateExpiration do |pages_domain| - pages_domain - end - end - - class PagesDomain < Grape::Entity - expose :domain - expose :url - expose :verified?, as: :verified - expose :verification_code, as: :verification_code - expose :enabled_until - expose :auto_ssl_enabled - - expose :certificate, - if: ->(pages_domain, _) { pages_domain.certificate? }, - using: PagesDomainCertificate do |pages_domain| - pages_domain - end - end - class Application < Grape::Entity expose :id expose :uid, as: :application_id @@ -437,49 +244,6 @@ module API expose :project_id end - class BasicBadgeDetails < Grape::Entity - expose :name - expose :link_url - expose :image_url - expose :rendered_link_url do |badge, options| - badge.rendered_link_url(options.fetch(:project, nil)) - end - expose :rendered_image_url do |badge, options| - badge.rendered_image_url(options.fetch(:project, nil)) - end - end - - class Badge < BasicBadgeDetails - expose :id - expose :kind do |badge| - badge.type == 'ProjectBadge' ? 'project' : 'group' - end - end - - class ResourceLabelEvent < Grape::Entity - expose :id - expose :user, using: Entities::UserBasic - expose :created_at - expose :resource_type do |event, options| - event.issuable.class.name - end - expose :resource_id do |event, options| - event.issuable.id - end - expose :label, using: Entities::LabelBasic - expose :action - end - - class Suggestion < Grape::Entity - expose :id - expose :from_line - expose :to_line - expose :appliable?, as: :appliable - expose :applied - expose :from_content - expose :to_content - end - module Platform class Kubernetes < Grape::Entity expose :api_url @@ -501,23 +265,6 @@ module API end end - class Cluster < Grape::Entity - expose :id, :name, :created_at, :domain - expose :provider_type, :platform_type, :environment_scope, :cluster_type - expose :user, using: Entities::UserBasic - expose :platform_kubernetes, using: Entities::Platform::Kubernetes - expose :provider_gcp, using: Entities::Provider::Gcp - expose :management_project, using: Entities::ProjectIdentity - end - - class ClusterProject < Cluster - expose :project, using: Entities::BasicProjectDetails - end - - class ClusterGroup < Cluster - expose :group, using: Entities::BasicGroupDetails - end - module InternalPostReceive class Message < Grape::Entity expose :message diff --git a/lib/api/entities/badge.rb b/lib/api/entities/badge.rb new file mode 100644 index 00000000000..1e3e2ec469a --- /dev/null +++ b/lib/api/entities/badge.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class Badge < Entities::BasicBadgeDetails + expose :id + expose :kind do |badge| + badge.type == 'ProjectBadge' ? 'project' : 'group' + end + end + end +end diff --git a/lib/api/entities/basic_badge_details.rb b/lib/api/entities/basic_badge_details.rb new file mode 100644 index 00000000000..273dc57fe67 --- /dev/null +++ b/lib/api/entities/basic_badge_details.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class BasicBadgeDetails < Grape::Entity + expose :name + expose :link_url + expose :image_url + expose :rendered_link_url do |badge, options| + badge.rendered_link_url(options.fetch(:project, nil)) + end + expose :rendered_image_url do |badge, options| + badge.rendered_image_url(options.fetch(:project, nil)) + end + end + end +end diff --git a/lib/api/entities/cluster.rb b/lib/api/entities/cluster.rb new file mode 100644 index 00000000000..4cb54e988ce --- /dev/null +++ b/lib/api/entities/cluster.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class Cluster < Grape::Entity + expose :id, :name, :created_at, :domain + expose :provider_type, :platform_type, :environment_scope, :cluster_type + expose :user, using: Entities::UserBasic + expose :platform_kubernetes, using: Entities::Platform::Kubernetes + expose :provider_gcp, using: Entities::Provider::Gcp + expose :management_project, using: Entities::ProjectIdentity + end + end +end diff --git a/lib/api/entities/cluster_group.rb b/lib/api/entities/cluster_group.rb new file mode 100644 index 00000000000..8f71438cf3d --- /dev/null +++ b/lib/api/entities/cluster_group.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ClusterGroup < Entities::Cluster + expose :group, using: Entities::BasicGroupDetails + end + end +end diff --git a/lib/api/entities/cluster_project.rb b/lib/api/entities/cluster_project.rb new file mode 100644 index 00000000000..2fd3e35e2a2 --- /dev/null +++ b/lib/api/entities/cluster_project.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ClusterProject < Entities::Cluster + expose :project, using: Entities::BasicProjectDetails + end + end +end diff --git a/lib/api/entities/deployment.rb b/lib/api/entities/deployment.rb new file mode 100644 index 00000000000..3a97d3e3c09 --- /dev/null +++ b/lib/api/entities/deployment.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Deployment < Grape::Entity + expose :id, :iid, :ref, :sha, :created_at, :updated_at + expose :user, using: Entities::UserBasic + expose :environment, using: Entities::EnvironmentBasic + expose :deployable, using: Entities::Job + expose :status + end + end +end diff --git a/lib/api/entities/environment.rb b/lib/api/entities/environment.rb new file mode 100644 index 00000000000..cb39ce1b13a --- /dev/null +++ b/lib/api/entities/environment.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class Environment < Entities::EnvironmentBasic + expose :project, using: Entities::BasicProjectDetails + expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } + expose :state + end + end +end diff --git a/lib/api/entities/environment_basic.rb b/lib/api/entities/environment_basic.rb new file mode 100644 index 00000000000..061d4739874 --- /dev/null +++ b/lib/api/entities/environment_basic.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class EnvironmentBasic < Grape::Entity + expose :id, :name, :slug, :external_url + end + end +end diff --git a/lib/api/entities/group_access.rb b/lib/api/entities/group_access.rb new file mode 100644 index 00000000000..5e53e9645c2 --- /dev/null +++ b/lib/api/entities/group_access.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module API + module Entities + class GroupAccess < MemberAccess + end + end +end diff --git a/lib/api/entities/job_request/artifacts.rb b/lib/api/entities/job_request/artifacts.rb new file mode 100644 index 00000000000..c6871fdd875 --- /dev/null +++ b/lib/api/entities/job_request/artifacts.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Artifacts < Grape::Entity + expose :name + expose :untracked + expose :paths + expose :when + expose :expire_in + expose :artifact_type + expose :artifact_format + end + end + end +end diff --git a/lib/api/entities/job_request/cache.rb b/lib/api/entities/job_request/cache.rb new file mode 100644 index 00000000000..a75affbaf84 --- /dev/null +++ b/lib/api/entities/job_request/cache.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Cache < Grape::Entity + expose :key, :untracked, :paths, :policy + end + end + end +end diff --git a/lib/api/entities/job_request/credentials.rb b/lib/api/entities/job_request/credentials.rb new file mode 100644 index 00000000000..cdac5566cbd --- /dev/null +++ b/lib/api/entities/job_request/credentials.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Credentials < Grape::Entity + expose :type, :url, :username, :password + end + end + end +end diff --git a/lib/api/entities/job_request/dependency.rb b/lib/api/entities/job_request/dependency.rb new file mode 100644 index 00000000000..64d779f6575 --- /dev/null +++ b/lib/api/entities/job_request/dependency.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Dependency < Grape::Entity + expose :id, :name, :token + expose :artifacts_file, using: Entities::JobArtifactFile, if: ->(job, _) { job.artifacts? } + end + end + end +end diff --git a/lib/api/entities/job_request/git_info.rb b/lib/api/entities/job_request/git_info.rb new file mode 100644 index 00000000000..e07099263b5 --- /dev/null +++ b/lib/api/entities/job_request/git_info.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class GitInfo < Grape::Entity + expose :repo_url, :ref, :sha, :before_sha + expose :ref_type + expose :refspecs + expose :git_depth, as: :depth + end + end + end +end diff --git a/lib/api/entities/job_request/image.rb b/lib/api/entities/job_request/image.rb new file mode 100644 index 00000000000..47f4542d2d5 --- /dev/null +++ b/lib/api/entities/job_request/image.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Image < Grape::Entity + expose :name, :entrypoint + expose :ports, using: Entities::JobRequest::Port + end + end + end +end diff --git a/lib/api/entities/job_request/job_info.rb b/lib/api/entities/job_request/job_info.rb new file mode 100644 index 00000000000..09c13aa8471 --- /dev/null +++ b/lib/api/entities/job_request/job_info.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class JobInfo < Grape::Entity + expose :name, :stage + expose :project_id, :project_name + end + end + end +end diff --git a/lib/api/entities/job_request/port.rb b/lib/api/entities/job_request/port.rb new file mode 100644 index 00000000000..ee427da8657 --- /dev/null +++ b/lib/api/entities/job_request/port.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Port < Grape::Entity + expose :number, :protocol, :name + end + end + end +end diff --git a/lib/api/entities/job_request/response.rb b/lib/api/entities/job_request/response.rb new file mode 100644 index 00000000000..fdacd3af2da --- /dev/null +++ b/lib/api/entities/job_request/response.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Response < Grape::Entity + expose :id + expose :token + expose :allow_git_fetch + + expose :job_info, using: Entities::JobRequest::JobInfo do |model| + model + end + + expose :git_info, using: Entities::JobRequest::GitInfo do |model| + model + end + + expose :runner_info, using: Entities::JobRequest::RunnerInfo do |model| + model + end + + expose :variables + expose :steps, using: Entities::JobRequest::Step + expose :image, using: Entities::JobRequest::Image + expose :services, using: Entities::JobRequest::Service + expose :artifacts, using: Entities::JobRequest::Artifacts + expose :cache, using: Entities::JobRequest::Cache + expose :credentials, using: Entities::JobRequest::Credentials + expose :all_dependencies, as: :dependencies, using: Entities::JobRequest::Dependency + expose :features + end + end + end +end diff --git a/lib/api/entities/job_request/runner_info.rb b/lib/api/entities/job_request/runner_info.rb new file mode 100644 index 00000000000..e6d2e8d9e85 --- /dev/null +++ b/lib/api/entities/job_request/runner_info.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class RunnerInfo < Grape::Entity + expose :metadata_timeout, as: :timeout + expose :runner_session_url + end + end + end +end diff --git a/lib/api/entities/job_request/service.rb b/lib/api/entities/job_request/service.rb new file mode 100644 index 00000000000..9ad5abf4e9e --- /dev/null +++ b/lib/api/entities/job_request/service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Service < Entities::JobRequest::Image + expose :alias, :command + end + end + end +end diff --git a/lib/api/entities/job_request/step.rb b/lib/api/entities/job_request/step.rb new file mode 100644 index 00000000000..498dd017fb4 --- /dev/null +++ b/lib/api/entities/job_request/step.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Step < Grape::Entity + expose :name, :script, :timeout, :when, :allow_failure + end + end + end +end diff --git a/lib/api/entities/license.rb b/lib/api/entities/license.rb new file mode 100644 index 00000000000..d7a414344c1 --- /dev/null +++ b/lib/api/entities/license.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class License < Entities::LicenseBasic + expose :popular?, as: :popular + expose(:description) { |license| license.meta['description'] } + expose(:conditions) { |license| license.meta['conditions'] } + expose(:permissions) { |license| license.meta['permissions'] } + expose(:limitations) { |license| license.meta['limitations'] } + expose :content + end + end +end diff --git a/lib/api/entities/license_basic.rb b/lib/api/entities/license_basic.rb new file mode 100644 index 00000000000..08af68785a9 --- /dev/null +++ b/lib/api/entities/license_basic.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class LicenseBasic < Grape::Entity + expose :key, :name, :nickname + expose :url, as: :html_url + expose(:source_url) { |license| license.meta['source'] } + end + end +end diff --git a/lib/api/entities/member_access.rb b/lib/api/entities/member_access.rb new file mode 100644 index 00000000000..097c72bf617 --- /dev/null +++ b/lib/api/entities/member_access.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class MemberAccess < Grape::Entity + expose :access_level + expose :notification_level do |member, options| + if member.notification_setting + ::NotificationSetting.levels[member.notification_setting.level] + end + end + end + end +end diff --git a/lib/api/entities/namespace.rb b/lib/api/entities/namespace.rb new file mode 100644 index 00000000000..b21dd29c7b4 --- /dev/null +++ b/lib/api/entities/namespace.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Namespace < Entities::NamespaceBasic + expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _| + namespace.users_with_descendants.count + end + + def expose_members_count_with_descendants?(namespace, opts) + namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace) + end + end + end +end diff --git a/lib/api/entities/notification_setting.rb b/lib/api/entities/notification_setting.rb new file mode 100644 index 00000000000..cdff4f2f5c5 --- /dev/null +++ b/lib/api/entities/notification_setting.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class NotificationSetting < Grape::Entity + expose :level + expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do + ::NotificationSetting.email_events.each do |event| + expose event + end + end + end + end +end diff --git a/lib/api/entities/pages_domain.rb b/lib/api/entities/pages_domain.rb new file mode 100644 index 00000000000..87af8c7b0a4 --- /dev/null +++ b/lib/api/entities/pages_domain.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomain < Grape::Entity + expose :domain + expose :url + expose :verified?, as: :verified + expose :verification_code, as: :verification_code + expose :enabled_until + expose :auto_ssl_enabled + + expose :certificate, + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: Entities::PagesDomainCertificate do |pages_domain| + pages_domain + end + end + end +end diff --git a/lib/api/entities/pages_domain_basic.rb b/lib/api/entities/pages_domain_basic.rb new file mode 100644 index 00000000000..6f8901fe715 --- /dev/null +++ b/lib/api/entities/pages_domain_basic.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomainBasic < Grape::Entity + expose :domain + expose :url + expose :project_id + expose :verified?, as: :verified + expose :verification_code, as: :verification_code + expose :enabled_until + expose :auto_ssl_enabled + + expose :certificate, + as: :certificate_expiration, + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: Entities::PagesDomainCertificateExpiration do |pages_domain| + pages_domain + end + end + end +end diff --git a/lib/api/entities/pages_domain_certificate.rb b/lib/api/entities/pages_domain_certificate.rb new file mode 100644 index 00000000000..82c4729d454 --- /dev/null +++ b/lib/api/entities/pages_domain_certificate.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomainCertificate < Grape::Entity + expose :subject + expose :expired?, as: :expired + expose :certificate + expose :certificate_text + end + end +end diff --git a/lib/api/entities/project_access.rb b/lib/api/entities/project_access.rb new file mode 100644 index 00000000000..29f85fda620 --- /dev/null +++ b/lib/api/entities/project_access.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectAccess < Entities::MemberAccess + end + end +end diff --git a/lib/api/entities/resource_label_event.rb b/lib/api/entities/resource_label_event.rb new file mode 100644 index 00000000000..890264abf93 --- /dev/null +++ b/lib/api/entities/resource_label_event.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + class ResourceLabelEvent < Grape::Entity + expose :id + expose :user, using: Entities::UserBasic + expose :created_at + expose :resource_type do |event, options| + event.issuable.class.name + end + expose :resource_id do |event, options| + event.issuable.id + end + expose :label, using: Entities::LabelBasic + expose :action + end + end +end diff --git a/lib/api/entities/suggestion.rb b/lib/api/entities/suggestion.rb new file mode 100644 index 00000000000..59f94099d7f --- /dev/null +++ b/lib/api/entities/suggestion.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Suggestion < Grape::Entity + expose :id + expose :from_line + expose :to_line + expose :appliable?, as: :appliable + expose :applied + expose :from_content + expose :to_content + end + end +end diff --git a/lib/gitlab/background_migration/recalculate_project_authorizations.rb b/lib/gitlab/background_migration/recalculate_project_authorizations.rb new file mode 100644 index 00000000000..3d2ce9fc10c --- /dev/null +++ b/lib/gitlab/background_migration/recalculate_project_authorizations.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # rubocop:disable Style/Documentation + class RecalculateProjectAuthorizations + def perform(user_ids) + user_ids.each do |user_id| + user = User.find_by(id: user_id) + + next unless user + + service = Users::RefreshAuthorizedProjectsService.new( + user, + incorrect_auth_found_callback: + ->(project_id, access_level) do + logger.info(message: 'Removing ProjectAuthorizations', + user_id: user.id, + project_id: project_id, + access_level: access_level) + end, + missing_auth_found_callback: + ->(project_id, access_level) do + logger.info(message: 'Creating ProjectAuthorizations', + user_id: user.id, + project_id: project_id, + access_level: access_level) + end + ) + + service.execute + end + end + + private + + def logger + @logger ||= Gitlab::BackgroundMigration::Logger.build + end + end + end +end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 17a7be311ca..4fc5bfddf0c 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -353,11 +353,7 @@ module Gitlab def fetch_blob(sha, path) return unless sha - # Load only patch_hard_limit_bytes number of bytes for the blob - # Because otherwise, it is too large to be displayed - Blob.lazy( - repository.project, sha, path, - blob_size_limit: Gitlab::Git::Diff.patch_hard_limit_bytes) + Blob.lazy(repository.project, sha, path) end def total_blob_lines(blob) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 48da838366f..0b999197cd8 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -130,8 +130,7 @@ module Gitlab # :skip is the number of commits to skip # :order is the commits order and allowed value is :none (default), :date, # :topo, or any combination of them (in an array). Commit ordering types - # are documented here: - # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant) + # are documented here: https://git-scm.com/docs/git-log#_commit_ordering def find_all(repo, options = {}) wrapped_gitaly_errors do Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 15318bc817a..b981b9cf1bc 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -324,6 +324,7 @@ module Gitlab request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.before = GitalyClient.timestamp(options[:before]) if options[:before] request.revision = encode_binary(options[:ref]) if options[:ref] + request.order = options[:order].upcase if options[:order].present? request.paths = encode_repeated(Array(options[:path])) if options[:path].present? diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb index e2271b1492c..d65e8759ec9 100644 --- a/lib/gitlab/project_authorizations.rb +++ b/lib/gitlab/project_authorizations.rb @@ -68,12 +68,10 @@ module Gitlab .select([namespaces[:id], members[:access_level]]) .except(:order) - if Feature.enabled?(:share_group_with_group, default_enabled: true) - # Namespaces shared with any of the group - cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level']) - .joins(join_group_group_links) - .joins(join_members_on_group_group_links) - end + # Namespaces shared with any of the group + cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level']) + .joins(join_group_group_links) + .joins(join_members_on_group_group_links) # Sub groups of any groups the user is a member of. cte << Group.select([ @@ -114,6 +112,8 @@ module Gitlab members = Member.arel_table cond = group_group_links[:shared_with_group_id].eq(members[:source_id]) + .and(members[:source_type].eq('Namespace')) + .and(members[:requested_at].eq(nil)) .and(members[:user_id].eq(user.id)) Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond)) end diff --git a/package.json b/package.json index b6c2d56ca0a..3a5f4edb4ef 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,6 @@ "yarn-deduplicate": "^1.1.1" }, "resolutions": { - "at.js": "https://gitlab.com/gitlab-org/frontend/At.js.git#121ce9a557b51c33f5693ac8df52d2bda1e53cbe", "vue-jest/ts-jest": "24.0.0", "monaco-editor": "0.18.1" }, diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb index 277e7364ada..b0b511eb23a 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Create', :smoke do + context 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/205511', type: :bug } do describe 'Snippet creation' do it 'User creates a snippet' do Flow::Login.sign_in diff --git a/spec/factories/project_authorizations.rb b/spec/factories/project_authorizations.rb new file mode 100644 index 00000000000..ffdf5576f84 --- /dev/null +++ b/spec/factories/project_authorizations.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :project_authorization do + user + project + access_level { Gitlab::Access::REPORTER } + end +end diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js index 1de496ba3f8..3a4dcc5873d 100644 --- a/spec/frontend/ide/components/error_message_spec.js +++ b/spec/frontend/ide/components/error_message_spec.js @@ -1,4 +1,4 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import ErrorMessage from '~/ide/components/error_message.vue'; @@ -15,7 +15,7 @@ describe('IDE error message component', () => { actions: { setErrorMessage: setErrorMessageMock }, }); - wrapper = shallowMount(ErrorMessage, { + wrapper = mount(ErrorMessage, { propsData: { message: { text: 'some text', @@ -38,15 +38,18 @@ describe('IDE error message component', () => { wrapper = null; }); + const findDismissButton = () => wrapper.find('button[aria-label=Dismiss]'); + const findActionButton = () => wrapper.find('button.gl-alert-action'); + it('renders error message', () => { const text = 'error message'; createComponent({ text }); expect(wrapper.text()).toContain(text); }); - it('clears error message on click', () => { + it('clears error message on dismiss click', () => { createComponent(); - wrapper.trigger('click'); + findDismissButton().trigger('click'); expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null, undefined); }); @@ -68,29 +71,27 @@ describe('IDE error message component', () => { }); it('renders action button', () => { - const button = wrapper.find('button'); + const button = findActionButton(); expect(button.exists()).toBe(true); expect(button.text()).toContain(message.actionText); }); - it('does not clear error message on click', () => { - wrapper.trigger('click'); - - expect(setErrorMessageMock).not.toHaveBeenCalled(); + it('does not show dismiss button', () => { + expect(findDismissButton().exists()).toBe(false); }); it('dispatches action', () => { - wrapper.find('button').trigger('click'); + findActionButton().trigger('click'); expect(actionMock).toHaveBeenCalledWith(message.actionPayload); }); it('does not dispatch action when already loading', () => { - wrapper.find('button').trigger('click'); + findActionButton().trigger('click'); actionMock.mockReset(); return wrapper.vm.$nextTick(() => { - wrapper.find('button').trigger('click'); + findActionButton().trigger('click'); return wrapper.vm.$nextTick().then(() => { expect(actionMock).not.toHaveBeenCalled(); @@ -106,7 +107,7 @@ describe('IDE error message component', () => { resolveAction = resolve; }), ); - wrapper.find('button').trigger('click'); + findActionButton().trigger('click'); return wrapper.vm.$nextTick(() => { expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); @@ -115,7 +116,7 @@ describe('IDE error message component', () => { }); it('hides loading icon when operation finishes', () => { - wrapper.find('button').trigger('click'); + findActionButton().trigger('click'); return actionMock() .then(() => wrapper.vm.$nextTick()) .then(() => { diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 048db4a7533..4241b994cba 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -61,14 +61,14 @@ describe('ide component, non-empty repo', () => { }); it('shows error message when set', done => { - expect(vm.$el.querySelector('.flash-container')).toBe(null); + expect(vm.$el.querySelector('.gl-alert')).toBe(null); vm.$store.state.errorMessage = { text: 'error', }; vm.$nextTick(() => { - expect(vm.$el.querySelector('.flash-container')).not.toBe(null); + expect(vm.$el.querySelector('.gl-alert')).not.toBe(null); done(); }); diff --git a/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js b/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js index a8d39b7b5fe..5f432f2a1b5 100644 --- a/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js +++ b/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js @@ -1,149 +1,160 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; -const TEST_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; -const STYLE_TRUNCATED = 'display: inline-block; max-width: 20px;'; -const STYLE_NORMAL = 'display: inline-block; max-width: 1000px;'; +const TEXT_SHORT = 'lorem'; +const TEXT_LONG = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; -const localVue = createLocalVue(); +const TEXT_TRUNCATE = 'white-space: nowrap; overflow:hidden;'; +const STYLE_NORMAL = `${TEXT_TRUNCATE} display: inline-block; max-width: 1000px;`; // does not overflows +const STYLE_OVERFLOWED = `${TEXT_TRUNCATE} display: inline-block; max-width: 50px;`; // overflowed when text is long const createElementWithStyle = (style, content) => `<a href="#" style="${style}">${content}</a>`; describe('TooltipOnTruncate component', () => { let wrapper; + let parent; const createComponent = ({ propsData, ...options } = {}) => { - wrapper = shallowMount(localVue.extend(TooltipOnTruncate), { - localVue, + wrapper = shallowMount(TooltipOnTruncate, { attachToDocument: true, propsData: { - title: TEST_TITLE, ...propsData, }, + attrs: { + style: STYLE_OVERFLOWED, + }, ...options, }); }; + const createWrappedComponent = ({ propsData, ...options }) => { + // set a parent around the tested component + parent = mount( + { + props: { + title: { default: '' }, + }, + template: ` + <TooltipOnTruncate :title="title" truncate-target="child" style="${STYLE_OVERFLOWED}"> + <div>{{title}}</div> + </TooltipOnTruncate> + `, + components: { + TooltipOnTruncate, + }, + }, + { + propsData: { ...propsData }, + attachToDocument: true, + ...options, + }, + ); + + wrapper = parent.find(TooltipOnTruncate); + }; + + const hasTooltip = () => wrapper.classes('js-show-tooltip'); + afterEach(() => { wrapper.destroy(); }); - const hasTooltip = () => wrapper.classes('js-show-tooltip'); - describe('with default target', () => { - it('renders tooltip if truncated', done => { + it('renders tooltip if truncated', () => { createComponent({ - attrs: { - style: STYLE_TRUNCATED, + propsData: { + title: TEXT_LONG, }, slots: { - default: [TEST_TITLE], + default: [TEXT_LONG], }, }); - wrapper.vm - .$nextTick() - .then(() => { - expect(hasTooltip()).toBe(true); - expect(wrapper.attributes('data-original-title')).toEqual(TEST_TITLE); - expect(wrapper.attributes('data-placement')).toEqual('top'); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(hasTooltip()).toBe(true); + expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG); + expect(wrapper.attributes('data-placement')).toEqual('top'); + }); }); - it('does not render tooltip if normal', done => { + it('does not render tooltip if normal', () => { createComponent({ - attrs: { - style: STYLE_NORMAL, + propsData: { + title: TEXT_SHORT, }, slots: { - default: [TEST_TITLE], + default: [TEXT_SHORT], }, }); - wrapper.vm - .$nextTick() - .then(() => { - expect(hasTooltip()).toBe(false); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(hasTooltip()).toBe(false); + }); }); }); describe('with child target', () => { - it('renders tooltip if truncated', done => { + it('renders tooltip if truncated', () => { createComponent({ attrs: { style: STYLE_NORMAL, }, propsData: { + title: TEXT_LONG, truncateTarget: 'child', }, slots: { - default: createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE), + default: createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG), }, }); - wrapper.vm - .$nextTick() - .then(() => { - expect(hasTooltip()).toBe(true); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(hasTooltip()).toBe(true); + }); }); - it('does not render tooltip if normal', done => { + it('does not render tooltip if normal', () => { createComponent({ propsData: { truncateTarget: 'child', }, slots: { - default: createElementWithStyle(STYLE_NORMAL, TEST_TITLE), + default: createElementWithStyle(STYLE_NORMAL, TEXT_LONG), }, }); - wrapper.vm - .$nextTick() - .then(() => { - expect(hasTooltip()).toBe(false); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(hasTooltip()).toBe(false); + }); }); }); describe('with fn target', () => { - it('renders tooltip if truncated', done => { + it('renders tooltip if truncated', () => { createComponent({ attrs: { style: STYLE_NORMAL, }, propsData: { + title: TEXT_LONG, truncateTarget: el => el.childNodes[1], }, slots: { default: [ - createElementWithStyle('', TEST_TITLE), - createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE), + createElementWithStyle('', TEXT_LONG), + createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG), ], }, }); - wrapper.vm - .$nextTick() - .then(() => { - expect(hasTooltip()).toBe(true); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(hasTooltip()).toBe(true); + }); }); }); describe('placement', () => { - it('sets data-placement when tooltip is rendered', done => { + it('sets data-placement when tooltip is rendered', () => { const placement = 'bottom'; createComponent({ @@ -151,21 +162,75 @@ describe('TooltipOnTruncate component', () => { placement, }, attrs: { - style: STYLE_TRUNCATED, + style: STYLE_OVERFLOWED, }, slots: { - default: TEST_TITLE, + default: TEXT_LONG, }, }); - wrapper.vm - .$nextTick() - .then(() => { - expect(hasTooltip()).toBe(true); - expect(wrapper.attributes('data-placement')).toEqual(placement); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(hasTooltip()).toBe(true); + expect(wrapper.attributes('data-placement')).toEqual(placement); + }); + }); + }); + + describe('updates when title and slot content changes', () => { + describe('is initialized with a long text', () => { + beforeEach(() => { + createWrappedComponent({ + propsData: { title: TEXT_LONG }, + }); + return parent.vm.$nextTick(); + }); + + it('renders tooltip', () => { + expect(hasTooltip()).toBe(true); + expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG); + expect(wrapper.attributes('data-placement')).toEqual('top'); + }); + + it('does not render tooltip after updated to a short text', () => { + parent.setProps({ + title: TEXT_SHORT, + }); + + return wrapper.vm + .$nextTick() + .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot + .then(() => { + expect(hasTooltip()).toBe(false); + }); + }); + }); + + describe('is initialized with a short text', () => { + beforeEach(() => { + createWrappedComponent({ + propsData: { title: TEXT_SHORT }, + }); + return wrapper.vm.$nextTick(); + }); + + it('does not render tooltip', () => { + expect(hasTooltip()).toBe(false); + }); + + it('renders tooltip after updated to a long text', () => { + parent.setProps({ + title: TEXT_LONG, + }); + + return wrapper.vm + .$nextTick() + .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot + .then(() => { + expect(hasTooltip()).toBe(true); + expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG); + expect(wrapper.attributes('data-placement')).toEqual('top'); + }); + }); }); }); }); diff --git a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb new file mode 100644 index 00000000000..1ef2c451aa2 --- /dev/null +++ b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, :migration, schema: 20200204113223 do + let(:users_table) { table(:users) } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_authorizations_table) { table(:project_authorizations) } + let(:members_table) { table(:members) } + let(:group_group_links) { table(:group_group_links) } + let(:project_group_links) { table(:project_group_links) } + + let(:user) { users_table.create!(id: 1, email: 'user@example.com', projects_limit: 10) } + let(:group) { namespaces_table.create!(type: 'Group', name: 'group', path: 'group') } + + subject { described_class.new.perform([user.id]) } + + context 'missing authorization' do + context 'personal project' do + before do + user_namespace = namespaces_table.create!(owner_id: user.id, name: 'User', path: 'user') + projects_table.create!(id: 1, + name: 'personal-project', + path: 'personal-project', + visibility_level: 0, + namespace_id: user_namespace.id) + end + + it 'creates correct authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 40)])) + end + end + + context 'group membership' do + before do + projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: group.id) + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 20, notification_level: 3) + end + + it 'creates correct authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) + end + end + + context 'inherited group membership' do + before do + sub_group = namespaces_table.create!(type: 'Group', name: 'subgroup', + path: 'subgroup', parent_id: group.id) + projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: sub_group.id) + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 20, notification_level: 3) + end + + it 'creates correct authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) + end + end + + context 'project membership' do + before do + project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: group.id) + members_table.create!(user_id: user.id, source_id: project.id, source_type: 'Project', + type: 'ProjectMember', access_level: 20, notification_level: 3) + end + + it 'creates correct authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) + end + end + + context 'shared group' do + before do + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 30, notification_level: 3) + + shared_group = namespaces_table.create!(type: 'Group', name: 'shared group', + path: 'shared-group') + projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0, + namespace_id: shared_group.id) + + group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id, + group_access: 20) + end + + it 'creates correct authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) + end + end + + context 'shared project' do + before do + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 30, notification_level: 3) + + another_group = namespaces_table.create!(type: 'Group', name: 'another group', path: 'another-group') + shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project', + visibility_level: 0, namespace_id: another_group.id) + + project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20) + end + + it 'creates correct authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(0).to(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)])) + end + end + end + + context 'unapproved access requests' do + context 'group membership' do + before do + projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: group.id) + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 20, requested_at: Time.now, notification_level: 3) + end + + it 'does not create authorization' do + expect { subject }.not_to change { project_authorizations_table.count }.from(0) + end + end + + context 'inherited group membership' do + before do + sub_group = namespaces_table.create!(type: 'Group', name: 'subgroup', path: 'subgroup', + parent_id: group.id) + projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: sub_group.id) + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 20, requested_at: Time.now, notification_level: 3) + end + + it 'does not create authorization' do + expect { subject }.not_to change { project_authorizations_table.count }.from(0) + end + end + + context 'project membership' do + before do + project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: group.id) + members_table.create!(user_id: user.id, source_id: project.id, source_type: 'Project', + type: 'ProjectMember', access_level: 20, requested_at: Time.now, notification_level: 3) + end + + it 'does not create authorization' do + expect { subject }.not_to change { project_authorizations_table.count }.from(0) + end + end + + context 'shared group' do + before do + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 30, requested_at: Time.now, notification_level: 3) + + shared_group = namespaces_table.create!(type: 'Group', name: 'shared group', + path: 'shared-group') + projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0, + namespace_id: shared_group.id) + + group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id, + group_access: 20) + end + + it 'does not create authorization' do + expect { subject }.not_to change { project_authorizations_table.count }.from(0) + end + end + + context 'shared project' do + before do + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 30, requested_at: Time.now, notification_level: 3) + + another_group = namespaces_table.create!(type: 'Group', name: 'another group', path: 'another-group') + shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project', + visibility_level: 0, namespace_id: another_group.id) + + project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20) + end + + it 'does not create authorization' do + expect { subject }.not_to change { project_authorizations_table.count }.from(0) + end + end + end + + context 'incorrect authorization' do + before do + project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: group.id) + members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace', + type: 'GroupMember', access_level: 30, notification_level: 3) + + project_authorizations_table.create!(user_id: user.id, project_id: project.id, + access_level: 10) + end + + it 'fixes authorization' do + expect { subject }.not_to change { project_authorizations_table.count }.from(1) + expect(project_authorizations_table.all).to( + match_array([have_attributes(user_id: 1, project_id: 1, access_level: 30)])) + end + end + + context 'unwanted authorization' do + before do + project = projects_table.create!(name: 'group-project', path: 'group-project', + visibility_level: 0, namespace_id: group.id) + + project_authorizations_table.create!(user_id: user.id, project_id: project.id, + access_level: 10) + end + + it 'deletes authorization' do + expect { subject }.to change { project_authorizations_table.count }.from(1).to(0) + end + end + + context 'deleted user' do + let(:nonexistent_user_id) { User.maximum(:id).to_i + 999 } + + it 'does not fail' do + expect { described_class.new.perform([nonexistent_user_id]) }.not_to raise_error + end + end +end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 4733607ccc0..61d7400b95e 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -175,7 +175,7 @@ describe Gitlab::Diff::File do [diff_file.new_content_sha, diff_file.new_path], [diff_file.old_content_sha, diff_file.old_path] ] - expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: 100 * 1024).and_call_original + expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: 10.megabytes).and_call_original old_data = diff_file.old_blob.data data = diff_file.new_blob.data diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 820578dfc6e..5e1d6199c3c 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -279,4 +279,19 @@ describe Gitlab::GitalyClient::CommitService do expect(subject.deletions).to eq(15) end end + + describe '#find_commits' do + it 'sends an RPC request' do + request = Gitaly::FindCommitsRequest.new( + repository: repository_message, + disable_walk: true, + order: 'TOPO' + ) + + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commits) + .with(request, kind_of(Hash)).and_return([]) + + client.find_commits(order: 'topo') + end + end end diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb index 6e5c36172e2..1c579128223 100644 --- a/spec/lib/gitlab/project_authorizations_spec.rb +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -97,87 +97,68 @@ describe Gitlab::ProjectAuthorizations do create(:group_group_link, shared_group: shared_group, shared_with_group: group) end - context 'when feature flag share_group_with_group is enabled' do - before do - stub_feature_flags(share_group_with_group: true) - end - - context 'group user' do - let(:user) { group_user } + context 'group user' do + let(:user) { group_user } - it 'creates proper authorizations' do - mapping = map_access_levels(authorizations) + it 'creates proper authorizations' do + mapping = map_access_levels(authorizations) - expect(mapping[project_parent.id]).to be_nil - expect(mapping[project.id]).to eq(Gitlab::Access::DEVELOPER) - expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER) - end + expect(mapping[project_parent.id]).to be_nil + expect(mapping[project.id]).to eq(Gitlab::Access::DEVELOPER) + expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER) end + end - context 'parent group user' do - let(:user) { parent_group_user } + context 'parent group user' do + let(:user) { parent_group_user } - it 'creates proper authorizations' do - mapping = map_access_levels(authorizations) + it 'creates proper authorizations' do + mapping = map_access_levels(authorizations) - expect(mapping[project_parent.id]).to be_nil - expect(mapping[project.id]).to be_nil - expect(mapping[project_child.id]).to be_nil - end + expect(mapping[project_parent.id]).to be_nil + expect(mapping[project.id]).to be_nil + expect(mapping[project_child.id]).to be_nil end + end - context 'child group user' do - let(:user) { child_group_user } + context 'child group user' do + let(:user) { child_group_user } - it 'creates proper authorizations' do - mapping = map_access_levels(authorizations) + it 'creates proper authorizations' do + mapping = map_access_levels(authorizations) - expect(mapping[project_parent.id]).to be_nil - expect(mapping[project.id]).to be_nil - expect(mapping[project_child.id]).to be_nil - end + expect(mapping[project_parent.id]).to be_nil + expect(mapping[project.id]).to be_nil + expect(mapping[project_child.id]).to be_nil end end - context 'when feature flag share_group_with_group is disabled' do - before do - stub_feature_flags(share_group_with_group: false) - end - - context 'group user' do - let(:user) { group_user } - - it 'creates proper authorizations' do - mapping = map_access_levels(authorizations) + context 'user without accepted access request' do + let!(:user) { create(:user) } - expect(mapping[project_parent.id]).to be_nil - expect(mapping[project.id]).to be_nil - expect(mapping[project_child.id]).to be_nil - end - end + it 'does not have access to group and its projects' do + create(:group_member, :developer, :access_request, user: user, group: group) - context 'parent group user' do - let(:user) { parent_group_user } + mapping = map_access_levels(authorizations) - it 'creates proper authorizations' do - mapping = map_access_levels(authorizations) - - expect(mapping[project_parent.id]).to be_nil - expect(mapping[project.id]).to be_nil - expect(mapping[project_child.id]).to be_nil - end + expect(mapping[project_parent.id]).to be_nil + expect(mapping[project.id]).to be_nil + expect(mapping[project_child.id]).to be_nil end + end - context 'child group user' do - let(:user) { child_group_user } + context 'unrelated project owner' do + let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 } + let!(:group) { create(:group, id: common_id) } + let!(:unrelated_project) { create(:project, id: common_id) } + let(:user) { unrelated_project.owner } - it 'creates proper authorizations' do - mapping = map_access_levels(authorizations) + it 'does not have access to group and its projects' do + mapping = map_access_levels(authorizations) - expect(mapping[project_parent.id]).to be_nil - expect(mapping[project.id]).to be_nil - expect(mapping[project_child.id]).to be_nil - end + expect(mapping[project_parent.id]).to be_nil + expect(mapping[project.id]).to be_nil + expect(mapping[project_child.id]).to be_nil end end end diff --git a/spec/migrations/schedule_recalculate_project_authorizations_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_spec.rb new file mode 100644 index 00000000000..77ad2b2dc8e --- /dev/null +++ b/spec/migrations/schedule_recalculate_project_authorizations_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200204113223_schedule_recalculate_project_authorizations.rb') + +describe ScheduleRecalculateProjectAuthorizations, :migration do + let(:users_table) { table(:users) } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_authorizations_table) { table(:project_authorizations) } + + let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 1) } + let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 1) } + let(:group) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') } + let(:project) do + projects_table.create!(id: 1, name: 'project', path: 'project', + visibility_level: 0, namespace_id: group.id) + end + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + + project_authorizations_table.create!(user_id: user1.id, project_id: project.id, access_level: 30) + project_authorizations_table.create!(user_id: user2.id, project_id: project.id, access_level: 30) + end + + it 'schedules background migration' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + expect(described_class::MIGRATION).to be_scheduled_migration([user1.id]) + expect(described_class::MIGRATION).to be_scheduled_migration([user2.id]) + end + end + end + + it 'ignores projects with higher id than maximum group id' do + another_user = users_table.create!(name: 'another user', email: 'another-user@example.com', + projects_limit: 1) + ignored_project = projects_table.create!(id: 2, name: 'ignored-project', path: 'ignored-project', + visibility_level: 0, namespace_id: group.id) + project_authorizations_table.create!(user_id: another_user.id, project_id: ignored_project.id, + access_level: 30) + + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + expect(described_class::MIGRATION).to be_scheduled_migration([user1.id]) + expect(described_class::MIGRATION).to be_scheduled_migration([user2.id]) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5c56d1aa757..79d9e42666c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1980,6 +1980,23 @@ describe Project do expect(project.reload.import_url).to eq('http://test.com') end + + it 'saves the url credentials percent decoded' do + url = 'http://user:pass%21%3F%40@github.com/t.git' + project = build(:project, import_url: url) + + # When the credentials are not decoded this expectation fails + expect(project.import_url).to eq(url) + expect(project.import_data.credentials).to eq(user: 'user', password: 'pass!?@') + end + + it 'saves url with no credentials' do + url = 'http://github.com/t.git' + project = build(:project, import_url: url) + + expect(project.import_url).to eq(url) + expect(project.import_data.credentials).to eq(user: nil, password: nil) + end end describe '#container_registry_url' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 0adf3fc8e85..00ffc3cae54 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -325,6 +325,14 @@ describe Repository do expect(repository.commits(nil, all: true, limit: 60).size).to eq(60) end end + + context "when 'order' flag is set" do + it 'passes order option to perform the query' do + expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(order: 'topo')).and_call_original + + repository.commits('master', limit: 1, order: 'topo') + end + end end describe '#new_commits' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e390f3945a9..170b9ccccf8 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -12,7 +12,6 @@ describe API::Commits do let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } - let(:project_id) { project.id } let(:current_user) { nil } @@ -241,6 +240,40 @@ describe API::Commits do end end end + + context 'with order parameter' do + let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" } + + context 'set to topo' do + let(:order) { 'topo' } + + # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876 + # * 0031876 + # |\ + # | * 48ca272 + # | * 335bc94 + # * | bf6e164 + # * | 9d526f8 + # |/ + # * 1039376 + it 'returns project commits ordered by topo order' do + commits = project.repository.commits("0031876", limit: 6, order: 'topo') + + get api(route, current_user) + + expect(json_response.size).to eq(6) + expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id)) + end + end + + context 'set to blank' do + let(:order) { '' } + + it_behaves_like '400 response' do + let(:request) { get api(route, current_user) } + end + end + end end end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 09e005398a9..19422d4ca39 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -795,13 +795,13 @@ describe API::Issues do it 'returns issues from non archived projects only by default' do get api("/groups/#{group1.id}/issues", user), params: { scope: 'all' } - expect_response_contain_exactly(issue2, issue1) + expect_paginated_array_response([issue2.id, issue1.id]) end it 'returns issues from archived and non archived projects when non_archived is false' do get api("/groups/#{group1.id}/issues", user), params: { non_archived: false, scope: 'all' } - expect_response_contain_exactly(issue1, issue2, issue3) + expect_paginated_array_response([issue3.id, issue2.id, issue1.id]) end end end @@ -888,9 +888,4 @@ describe API::Issues do include_examples 'time tracking endpoints', 'issue' end - - def expect_response_contain_exactly(*items) - expect(json_response.length).to eq(items.size) - expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id)) - end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 427a361295c..00af0937dd7 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -41,8 +41,7 @@ describe API::MergeRequests do it 'returns merge requests for public projects' do get api(endpoint_path) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array end end @@ -87,10 +86,11 @@ describe API::MergeRequests do it 'returns an array of all merge_requests' do get api(endpoint_path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) + expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) @@ -111,7 +111,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) + expect_successful_response_with_paginated_array expect(json_response.last['labels'].pluck('name')).to eq([label2.title, label.title]) expect(json_response.last['labels'].first).to match_schema('/public_api/v4/label_basic') end @@ -139,11 +139,11 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) expect(json_response.last['iid']).to eq(merge_request.iid) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') @@ -157,10 +157,10 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) expect(json_response.last['title']).to eq(merge_request.title) end @@ -169,10 +169,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request.id]) expect(json_response.last['title']).to eq(merge_request.title) end @@ -181,10 +178,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request_closed.id]) expect(json_response.first['title']).to eq(merge_request_closed.title) end @@ -193,10 +187,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request_merged.id]) expect(json_response.first['title']).to eq(merge_request_merged.title) end @@ -210,17 +201,13 @@ describe API::MergeRequests do it 'returns an empty array if no issue matches milestone' do get api(endpoint_path, user), params: { milestone: '1.0.0' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it 'returns an empty array if milestone does not exist' do get api(endpoint_path, user), params: { milestone: 'foo' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it 'returns an array of merge requests in given milestone' do @@ -234,9 +221,7 @@ describe API::MergeRequests do it 'returns an array of merge requests matching state in milestone' do get api(endpoint_path, user), params: { milestone: '0.9', state: 'closed' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request_closed.id]) expect(json_response.first['id']).to eq(merge_request_closed.id) end @@ -248,8 +233,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) end @@ -259,9 +243,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it 'returns an empty array if no merge request matches labels' do @@ -269,9 +251,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it 'returns an array of labeled merge requests where all labels match' do @@ -279,8 +259,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) end @@ -288,8 +267,7 @@ describe API::MergeRequests do it 'returns an array of merge requests with any label when filtering by any label' do get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] } - expect_paginated_array_response - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) expect(json_response.first['id']).to eq(merge_request.id) @@ -298,8 +276,7 @@ describe API::MergeRequests do it 'returns an array of merge requests with any label when filtering by any label' do get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] } - expect_paginated_array_response - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) expect(json_response.first['id']).to eq(merge_request.id) @@ -308,7 +285,7 @@ describe API::MergeRequests do it 'returns an array of merge requests with any label when filtering by any label' do get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY } - expect_paginated_array_response + expect_successful_response_with_paginated_array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request.id) end @@ -316,10 +293,9 @@ describe API::MergeRequests do it 'returns an array of merge requests without a label when filtering by no label' do get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE } - response_ids = json_response.map { |merge_request| merge_request['id'] } - - expect_paginated_array_response - expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, merge_request_closed.id + ]) end end @@ -339,10 +315,7 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(mr2.id) + expect_paginated_array_response([mr2.id]) end context 'with ordering' do @@ -356,10 +329,10 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect_paginated_array_response([ + merge_request_closed.id, merge_request_locked.id, + merge_request_merged.id, merge_request.id + ]) response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end @@ -369,10 +342,10 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect_paginated_array_response([ + merge_request.id, merge_request_merged.id, + merge_request_locked.id, merge_request_closed.id + ]) response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end @@ -414,10 +387,10 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect_paginated_array_response([ + merge_request.id, merge_request_locked.id, + merge_request_merged.id, merge_request_closed.id + ]) response_dates = json_response.map { |merge_request| merge_request['updated_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end @@ -427,10 +400,10 @@ describe API::MergeRequests do get api(path, user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(4) + expect_paginated_array_response([ + merge_request_closed.id, merge_request_locked.id, + merge_request_merged.id, merge_request.id + ]) response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end @@ -440,7 +413,9 @@ describe API::MergeRequests do it 'returns merge requests with the given source branch' do get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' } - expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, merge_request_closed.id + ]) end end @@ -448,7 +423,9 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' } - expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, merge_request_closed.id + ]) end end end @@ -471,7 +448,10 @@ describe API::MergeRequests do it 'returns an array of all merge requests' do get api('/merge_requests', user), params: { scope: 'all' } - expect_paginated_array_response + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) end it "returns authentication error without any scope" do @@ -507,30 +487,23 @@ describe API::MergeRequests do it 'returns an array of all merge requests except unauthorized ones' do get api('/merge_requests', user), params: { scope: :all } - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request_locked.id, merge_request2.id) + expect_paginated_array_response([ + merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id + ]) end it "returns an array of no merge_requests when wip=yes" do get api("/merge_requests", user), params: { wip: 'yes' } - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it "returns an array of no merge_requests when wip=no" do get api("/merge_requests", user), params: { wip: 'no' } - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request_locked.id, merge_request2.id) + expect_paginated_array_response([ + merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id + ]) end it 'does not return unauthorized merge requests' do @@ -539,7 +512,9 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { scope: :all } - expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request, merge_request_locked) + expect_paginated_array_response([ + merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id + ]) expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id) end @@ -548,7 +523,7 @@ describe API::MergeRequests do get api('/merge_requests', user2) - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests authored by the given user' do @@ -556,7 +531,7 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { author_id: user2.id, scope: :all } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests assigned to the given user' do @@ -564,7 +539,7 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests with no assignee' do @@ -572,7 +547,7 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { assignee_id: 'None', scope: :all } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests with any assignee' do @@ -581,7 +556,10 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { assignee_id: 'Any', scope: :all } - expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked) + expect_paginated_array_response([ + merge_request_merged.id, merge_request2.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) end it 'returns an array of merge requests assigned to me' do @@ -589,7 +567,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'assigned_to_me' } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests assigned to me (kebab-case)' do @@ -597,7 +575,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'assigned-to-me' } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests created by me' do @@ -605,7 +583,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'created_by_me' } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns an array of merge requests created by me (kebab-case)' do @@ -613,7 +591,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'created-by-me' } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end it 'returns merge requests reacted by the authenticated user by the given emoji' do @@ -622,14 +600,16 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' } - expect_response_ordered_exactly(merge_request3) + expect_paginated_array_response([merge_request3.id]) end context 'source_branch param' do it 'returns merge requests with the given source branch' do get api('/merge_requests', user), params: { source_branch: merge_request_closed.source_branch, state: 'all' } - expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, merge_request_closed.id + ]) end end @@ -637,7 +617,9 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api('/merge_requests', user), params: { target_branch: merge_request_closed.target_branch, state: 'all' } - expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, merge_request_closed.id + ]) end end @@ -646,7 +628,7 @@ describe API::MergeRequests do get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user) - expect_response_ordered_exactly(merge_request2) + expect_paginated_array_response([merge_request2.id]) end it 'returns merge requests created after a specific date' do @@ -654,7 +636,7 @@ describe API::MergeRequests do get api("/merge_requests?created_after=#{merge_request2.created_at}", user) - expect_response_ordered_exactly(merge_request2) + expect_paginated_array_response([merge_request2.id]) end it 'returns merge requests updated before a specific date' do @@ -662,7 +644,7 @@ describe API::MergeRequests do get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user) - expect_response_ordered_exactly(merge_request2) + expect_paginated_array_response([merge_request2.id]) end it 'returns merge requests updated after a specific date' do @@ -670,7 +652,7 @@ describe API::MergeRequests do get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user) - expect_response_ordered_exactly(merge_request2) + expect_paginated_array_response([merge_request2.id]) end context 'search params' do @@ -681,25 +663,25 @@ describe API::MergeRequests do it 'returns merge requests matching given search string for title' do get api("/merge_requests", user), params: { search: merge_request.title } - expect_response_ordered_exactly(merge_request) + expect_paginated_array_response([merge_request.id]) end it 'returns merge requests matching given search string for title and scoped in title' do get api("/merge_requests", user), params: { search: merge_request.title, in: 'title' } - expect_response_ordered_exactly(merge_request) + expect_paginated_array_response([merge_request.id]) end - it 'returns an empty array if no merge reques matches given search string for description and scoped in title' do + it 'returns an empty array if no merge request matches given search string for description and scoped in title' do get api("/merge_requests", user), params: { search: merge_request.description, in: 'title' } - expect_response_contain_exactly + expect_empty_array_response end it 'returns merge requests for project matching given search string for description' do get api("/merge_requests", user), params: { project_id: project.id, search: merge_request.description } - expect_response_ordered_exactly(merge_request) + expect_paginated_array_response([merge_request.id]) end end @@ -707,7 +689,7 @@ describe API::MergeRequests do it 'returns merge requests with the given state' do get api('/merge_requests', user), params: { state: 'locked' } - expect_response_contain_exactly(merge_request_locked) + expect_paginated_array_response([merge_request_locked.id]) end end end @@ -729,18 +711,13 @@ describe API::MergeRequests do it "returns an array of no merge_requests when wip=yes" do get api("/projects/#{project.id}/merge_requests", user), params: { wip: 'yes' } - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it 'returns merge_request by "iids" array' do get api(endpoint_path, user), params: { iids: [merge_request.iid, merge_request_closed.iid] } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) + expect_paginated_array_response([merge_request_closed.id, merge_request.id]) expect(json_response.first['title']).to eq merge_request_closed.title expect(json_response.first['id']).to eq merge_request_closed.id end @@ -815,12 +792,10 @@ describe API::MergeRequests do it 'returns an array excluding merge_requests from archived projects' do get api(endpoint_path, user) - expect_response_contain_exactly( - merge_request_merged, - merge_request_locked, - merge_request_closed, - merge_request - ) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) end context 'with non_archived param set as false' do @@ -829,13 +804,10 @@ describe API::MergeRequests do get api(path, user) - expect_response_contain_exactly( - merge_request_merged, - merge_request_locked, - merge_request_closed, - merge_request, - merge_request_archived - ) + expect_paginated_array_response([ + merge_request_merged.id, merge_request_archived.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ]) end end end @@ -1079,9 +1051,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) commit = merge_request.commits.first - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.size).to eq(merge_request.commits.size) expect(json_response.first['id']).to eq(commit.id) expect(json_response.first['title']).to eq(commit.title) @@ -1105,9 +1075,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/context_commits", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.size).to eq(merge_request.context_commits.size) expect(json_response.first['id']).to eq(context_commit.id) expect(json_response.first['title']).to eq(context_commit.title) @@ -1147,9 +1115,7 @@ describe API::MergeRequests do it 'returns a paginated array of corresponding pipelines' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines") - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.count).to eq(1) expect(json_response.first['id']).to eq(pipeline.id) end @@ -1395,7 +1361,7 @@ describe API::MergeRequests do expect(json_response['labels']).to eq([]) end - xit 'empty label param as array, does not add any labels' do + it 'empty label param as array, does not add any labels' do params[:labels] = [] post api("/projects/#{project.id}/merge_requests", user), params: params @@ -2232,7 +2198,7 @@ describe API::MergeRequests do expect(json_response['labels']).to eq [] end - xit 'empty label as array, removes labels' do + it 'empty label as array, removes labels' do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { title: 'new issue', @@ -2240,7 +2206,6 @@ describe API::MergeRequests do } expect(response.status).to eq(200) - # fails, as grape ommits for some reason empty array as optional param value, so nothing it passed along expect(json_response['labels']).to eq [] end @@ -2306,9 +2271,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) end @@ -2316,10 +2279,7 @@ describe API::MergeRequests do it 'returns an empty array when there are no issues to be closed' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) + expect_empty_array_response end it 'handles external issues' do @@ -2332,9 +2292,7 @@ describe API::MergeRequests do get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect_successful_response_with_paginated_array expect(json_response.length).to eq(2) expect(json_response.second['title']).to eq(ext_issue.title) expect(json_response.second['id']).to eq(ext_issue.id) @@ -2546,22 +2504,4 @@ describe API::MergeRequests do merge_request_closed.save merge_request_closed end - - def expect_response_contain_exactly(*items) - expect_paginated_array_response - expect(json_response.length).to eq(items.size) - expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id)) - end - - def expect_response_ordered_exactly(*items) - expect_paginated_array_response - expect(json_response.length).to eq(items.size) - expect(json_response.map { |element| element['id'] }).to eq(items.map(&:id)) - end - - def expect_paginated_array_response - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - end end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index f5a914bb482..d7ba7f5f69e 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -22,6 +22,42 @@ describe Users::RefreshAuthorizedProjectsService do service.execute end + + context 'callbacks' do + let(:callback) { double('callback') } + + context 'incorrect_auth_found_callback callback' do + let(:user) { create(:user) } + let(:service) do + described_class.new(user, + incorrect_auth_found_callback: callback) + end + + it 'is called' do + access_level = Gitlab::Access::DEVELOPER + create(:project_authorization, user: user, project: project, access_level: access_level) + + expect(callback).to receive(:call).with(project.id, access_level).once + + service.execute + end + end + + context 'missing_auth_found_callback callback' do + let(:service) do + described_class.new(user, + missing_auth_found_callback: callback) + end + + it 'is called' do + ProjectAuthorization.delete_all + + expect(callback).to receive(:call).with(project.id, Gitlab::Access::MAINTAINER).once + + service.execute + end + end + end end describe '#execute_without_lease' do diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index 4bf6a17c03e..44c38df71b0 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -40,6 +40,17 @@ module ApiHelpers end end + def expect_empty_array_response + expect_successful_response_with_paginated_array + expect(json_response.length).to eq(0) + end + + def expect_successful_response_with_paginated_array + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + end + def expect_paginated_array_response(items) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers diff --git a/yarn.lock b/yarn.lock index 3989da9f800..ce6a0e94fc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1774,9 +1774,10 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at.js@^1.5.4, "at.js@https://gitlab.com/gitlab-org/frontend/At.js.git#121ce9a557b51c33f5693ac8df52d2bda1e53cbe": +at.js@^1.5.4: version "1.5.4" - resolved "https://gitlab.com/gitlab-org/frontend/At.js.git#121ce9a557b51c33f5693ac8df52d2bda1e53cbe" + resolved "https://registry.yarnpkg.com/at.js/-/at.js-1.5.4.tgz#8fc60cc80eadbe4874449b166a818e7ae1d784c1" + integrity sha512-G8mgUb/PqShPoH8AyjuxsTGvIr1o716BtQUKDM44C8qN2W615y7KGJ68MlTGamd0J0D/m28emUkzagaHTdrGZw== atob@^2.1.1: version "2.1.2" |