Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6a9d7c009e
commit
d466ee5042
|
@ -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.
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>`,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
|
||||
|
|
|
@ -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-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-dropdown-item
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
:active="isOptionActive(option)"
|
||||
active-class="active"
|
||||
@click="setQuickRange(option)"
|
||||
<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"
|
||||
>
|
||||
<icon
|
||||
name="mobile-issue-close"
|
||||
class="align-bottom"
|
||||
:class="{ invisible: !isOptionActive(option) }"
|
||||
/>
|
||||
{{ option.label }}
|
||||
</gl-dropdown-item>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</gl-dropdown>
|
||||
<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 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>
|
||||
</tooltip-on-truncate>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add tooltip when dates in date picker are too long
|
||||
merge_request: 24664
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'API: Ability to list commits in order (--topo-order)'
|
||||
merge_request: 24702
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Web IDE alert message look and feel
|
||||
merge_request: 23300
|
||||
author: Sean Nichols
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Avoid double encoding of credential while importing a Project by URL
|
||||
merge_request: 24514
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix autocomplete limitation bug
|
||||
merge_request: 25127
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate access entities into own class files
|
||||
merge_request: 24845
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate environment entities into own class files
|
||||
merge_request: 24951
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate JobRequest entities into own class files
|
||||
merge_request: 24977
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate page domain entities into own class files
|
||||
merge_request: 24987
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate badge entities into own class files
|
||||
merge_request: 25116
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate cluster entities into own class files
|
||||
merge_request: 25121
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace underscore with lodash for ./app/assets/javascripts/serverless
|
||||
merge_request: 25011
|
||||
author: Tobias Spagert
|
||||
type: other
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ClusterGroup < Entities::Cluster
|
||||
expose :group, using: Entities::BasicGroupDetails
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ClusterProject < Entities::Cluster
|
||||
expose :project, using: Entities::BasicProjectDetails
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class GroupAccess < MemberAccess
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectAccess < Entities::MemberAccess
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :project_authorization do
|
||||
user
|
||||
project
|
||||
access_level { Gitlab::Access::REPORTER }
|
||||
end
|
||||
end
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
end
|
||||
|
||||
context 'parent group user' do
|
||||
let(:user) { parent_group_user }
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
context 'child group user' do
|
||||
let(:user) { child_group_user }
|
||||
|
||||
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 eq(Gitlab::Access::DEVELOPER)
|
||||
expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag share_group_with_group is disabled' do
|
||||
before do
|
||||
stub_feature_flags(share_group_with_group: false)
|
||||
context 'parent group user' do
|
||||
let(:user) { parent_group_user }
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
context 'group user' do
|
||||
let(:user) { 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 'parent group user' do
|
||||
let(:user) { parent_group_user }
|
||||
context 'user without accepted access request' do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
it 'creates proper authorizations' do
|
||||
mapping = map_access_levels(authorizations)
|
||||
it 'does not have access to group and its projects' do
|
||||
create(:group_member, :developer, :access_request, user: user, group: group)
|
||||
|
||||
expect(mapping[project_parent.id]).to be_nil
|
||||
expect(mapping[project.id]).to be_nil
|
||||
expect(mapping[project_child.id]).to be_nil
|
||||
end
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue