Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-30 12:09:21 +00:00
parent 06eadec552
commit f66bb12f38
30 changed files with 534 additions and 125 deletions

View File

@ -1 +1 @@
d5024702496569b6de051046e5e295c44de94be5
f085d841e2f5571b260910a454215bc1a7687bf0

View File

@ -1,10 +1,13 @@
<script>
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import { sortBy } from 'lodash';
import { GlAlert } from '@gitlab/ui';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import BoardColumnNew from './board_column_new.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import defaultSortableConfig from '~/sortable/sortable_config';
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
export default {
components: {
@ -36,6 +39,25 @@ export default {
? sortBy([...Object.values(this.boardLists)], 'position')
: this.lists;
},
canDragColumns() {
return this.glFeatures.graphqlBoardLists && this.canAdminList;
},
boardColumnWrapper() {
return this.canDragColumns ? Draggable : 'div';
},
draggableOptions() {
const options = {
...defaultSortableConfig,
disabled: this.disabled,
draggable: '.is-draggable',
fallbackOnBody: false,
group: 'boards-list',
tag: 'div',
value: this.lists,
};
return this.canDragColumns ? options : {};
},
},
mounted() {
if (this.glFeatures.graphqlBoardLists) {
@ -43,7 +65,26 @@ export default {
}
},
methods: {
...mapActions(['showPromotionList']),
...mapActions(['moveList', 'showPromotionList']),
handleDragOnStart() {
sortableStart();
},
handleDragOnEnd(params) {
sortableEnd();
const { item, newIndex, oldIndex, to } = params;
const listId = item.dataset.id;
const replacedListId = to.children[newIndex].dataset.id;
this.moveList({
listId,
replacedListId,
newIndex,
adjustmentValue: newIndex < oldIndex ? 1 : -1,
});
},
},
};
</script>
@ -53,16 +94,28 @@ export default {
<gl-alert v-if="error" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
<div v-if="!isSwimlanesOn" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap">
<component
:is="boardColumnWrapper"
v-if="!isSwimlanesOn"
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
data-qa-selector="boards_list"
@start="handleDragOnStart"
@end="handleDragOnEnd"
>
<board-column
v-for="list in boardListsToUse"
:key="list.id"
ref="board"
:can-admin-list="canAdminList"
:class="{
'is-draggable': !list.preset,
}"
:list="list"
:disabled="disabled"
/>
</div>
</component>
<template v-else>
<epics-swimlanes

View File

@ -180,6 +180,10 @@ export default {
{ state, commit, dispatch },
{ listId, replacedListId, newIndex, adjustmentValue },
) => {
if (listId === replacedListId) {
return;
}
const { boardLists } = state;
const backupList = { ...boardLists };
const movedList = boardLists[listId];

View File

@ -1,7 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
@ -13,12 +12,6 @@ export default {
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
},
mounted() {
this.patchAriaLabel();
},
updated() {
this.patchAriaLabel();
},
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
@ -26,17 +19,18 @@ export default {
'setRenderTreeList',
'setShowWhitespace',
]),
patchAriaLabel() {
this.$el
.querySelector('.js-show-diff-settings')
.setAttribute('aria-label', __('Diff view settings'));
},
},
};
</script>
<template>
<gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
<gl-dropdown
icon="settings"
:text="__('Diff view settings')"
:text-sr-only="true"
toggle-class="js-show-diff-settings"
right
>
<div class="gl-px-3">
<span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
<gl-button-group class="gl-display-flex">

View File

@ -235,10 +235,6 @@ h3.popover-header {
@extend .border-0;
}
&.card-without-margin {
margin: 0;
}
&.bg-light {
@extend .border-0;
}

View File

@ -234,6 +234,8 @@ ul.content-list {
}
}
ul.content-list.issuable-list > li,
ul.content-list.todos-list > li,
.card > .content-list > li {
padding: $gl-padding-top $gl-padding;
}

View File

@ -42,10 +42,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
def recent
@branches = @repository.recent_branches
end
def diverging_commit_counts
respond_to do |format|
format.json do

View File

@ -18,7 +18,9 @@ module Ci
return Ci::Pipeline.none
end
items = pipelines.no_child
items = pipelines
items = items.no_child unless params[:iids].present?
items = by_iids(items)
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
@ -52,6 +54,14 @@ module Ci
project.repository.tag_names
end
def by_iids(items)
if params[:iids].present?
items.for_iid(params[:iids])
else
items
end
end
def by_scope(items)
case params[:scope]
when 'running'

View File

@ -12,7 +12,9 @@ module Resolvers
def resolve(iid:)
BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args|
args[:key].all_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) }
finder = ::Ci::PipelinesFinder.new(project, context[:current_user], iids: iids)
finder.execute.each { |pipeline| loader.call(pipeline.iid.to_s, pipeline) }
end
end
end

View File

@ -85,9 +85,8 @@
- if @todos.any?
.js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
.card.card-without-border.card-without-margin
%ul.content-list.todos-list
= render @todos
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
.js-nothing-here-container.todos-all-done.hidden.svg-content
= image_tag 'illustrations/todos_all_done.svg'

View File

@ -1,7 +1,6 @@
- if @issues.to_a.any?
.card.card-without-border
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
= render partial: 'projects/issues/issue', collection: @issues
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
= render 'shared/empty_states/issues'

View File

@ -1,7 +1,6 @@
- if @merge_requests.to_a.any?
.card.card-without-border
%ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
%ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
= paginate @merge_requests, theme: "gitlab"

View File

@ -13,7 +13,7 @@
.dropdown-title.gl-display-flex
%span.gl-ml-auto
= _("Select type")
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ "aria-label" => _('Close') }
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') }
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
.dropdown-content
%ul

View File

@ -0,0 +1,6 @@
---
title: Adds type="button" to the close button for the issue type selector to prevent
accidental form submission
merge_request: 48249
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Remove Bootstrap 4's Cards components from Issuables and Todos
merge_request: 48004
author: Takuya Noguchi
type: performance

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
MESSAGE = <<~MESSAGE
## Pipeline Changes
This Merge Request contains changes to the pipeline configuration for the GitLab project.
Please consider the effect of the changes in this Merge Request on the following:
- Effects on different [pipeline types](https://docs.gitlab.com/ee/development/pipelines.html#pipelines-for-merge-requests)
- Effects on non-canonical projects (`gitlab-foss`, `security`, etc)
- Effects on [pipeline performance](https://about.gitlab.com/handbook/engineering/quality/performance-indicators/#average-merge-request-pipeline-duration-for-gitlab)
- Effects on fork pipelines
Please consider communicating these changes to the broader team following the [communication guideline for pipeline changes](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity-team/#pipeline-changes)
MESSAGE
if helper.has_ci_changes?
markdown(MESSAGE)
end

View File

@ -439,9 +439,11 @@ parameter, indicating a starting or ending point of our pagination. They should
followed with `first` or `last` parameter respectively to indicate _how many_ items
we want to fetch after or before a given endpoint.
For example, here we're fetching 10 designs after a cursor:
For example, here we're fetching 10 designs after a cursor (let us call this `projectQuery`):
```javascript
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query {
project(fullPath: "root/my-project") {
id
@ -453,6 +455,9 @@ query {
id
}
}
pageInfo {
...PageInfo
}
}
}
}
@ -460,21 +465,31 @@ query {
}
```
Note that we are using the [`pageInfo.fragment.graphql`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/graphql_shared/fragments/pageInfo.fragment.graphql) to populate the `pageInfo` information.
#### Using `fetchMore` method in components
This approach makes sense to use with user-handled pagination (e.g. when the scrolls to fetch more data or explicitly clicks a "Next Page"-button).
When we need to fetch all the data initially, it is recommended to use [a (non-smart) query, instead](#using-a-recursive-query-in-components).
When making an initial fetch, we usually want to start a pagination from the beginning.
In this case, we can either:
- Skip passing a cursor.
- Pass `null` explicitly to `after`.
After data is fetched, we should save a `pageInfo` object. Let's assume we're storing
it to Vue component `data`:
After data is fetched, we can use the `update`-hook as an opportunity [to customize
the data that is set in the Vue component property](https://apollo.vuejs.org/api/smart-query.html#options), getting a hold of the `pageInfo` object among other data.
In the `result`-hook, we can inspect the `pageInfo` object to see if we need to fetch
the next page. Note that we also keep a `requestCount` to ensure that the application
does not keep requesting the next page, indefinitely:
```javascript
data() {
return {
pageInfo: null,
requestCount: 0,
}
},
apollo: {
@ -482,13 +497,29 @@ apollo: {
query: projectQuery,
variables() {
return {
// rest of design variables
...
// ... The rest of the design variables
first: 10,
};
},
result(res) {
this.pageInfo = res.data?.project?.issue?.designCollection?.designs?.pageInfo;
update(data) {
const { id = null, issue = {} } = data.project || {};
const { edges = [], pageInfo } = issue.designCollection?.designs || {};
return {
id,
edges,
pageInfo,
};
},
result() {
const { pageInfo } = this.designs;
// Increment the request count with each new result
this.requestCount += 1;
// Only fetch next page if we have more requests and there is a next page to fetch
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.hasNextPage) {
this.fetchNextPage(pageInfo.endCursor);
}
},
},
},
@ -497,34 +528,102 @@ apollo: {
When we want to move to the next page, we use an Apollo `fetchMore` method, passing a
new cursor (and, optionally, new variables) there. In the `updateQuery` hook, we have
to return a result we want to see in the Apollo cache after fetching the next page.
[`Immer`s `produce`](#immutability-and-cache-updates)-function can help us with the immutability here:
```javascript
fetchNextPage() {
// as a first step, we're checking if we have more pages to move forward
if (this.pageInfo?.hasNextPage) {
this.$apollo.queries.designs.fetchMore({
variables: {
// rest of design variables
...
first: 10,
after: this.pageInfo?.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
// here we can implement the logic of adding new designs to fetched one (for example, if we use infinite scroll)
// or replacing old result with the new one if we use numbered pages
fetchNextPage(endCursor) {
this.$apollo.queries.designs.fetchMore({
variables: {
// ... The rest of the design variables
first: 10,
after: endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
// Here we can implement the logic of adding new designs to existing ones
// (for example, if we use infinite scroll) or replacing old result
// with the new one if we use numbered pages
const newDesigns = fetchMoreResult.project.issue.designCollection.designs;
previousResult.project.issue.designCollection.designs.push(...newDesigns)
const { designs: previousDesigns } = previousResult.project.issue.designCollection;
const { designs: newDesigns } = fetchMoreResult.project.issue.designCollection
return previousResult;
},
});
}
return produce(previousResult, draftData => {
// `produce` gives us a working copy, `draftData`, that we can modify
// as we please and from it will produce the next immutable result for us
draftData.project.issue.designCollection.designs = [...previousDesigns, ...newDesigns];
});
},
});
}
```
Please note we don't have to save `pageInfo` one more time; `fetchMore` triggers a query
`result` hook as well.
#### Using a recursive query in components
When it is necessary to fetch all paginated data initially an Apollo query can do the trick for us.
If we need to fetch the next page based on user interactions, it is recommend to use a [`smartQuery`](https://apollo.vuejs.org/api/smart-query.html) along with the [`fetchMore`-hook](#using-fetchmore-method-in-components).
When the query resolves we can update the component data and inspect the `pageInfo` object
to see if we need to fetch the next page, i.e. call the method recursively.
Note that we also keep a `requestCount` to ensure that the application does not keep
requesting the next page, indefinitely.
```javascript
data() {
return {
requestCount: 0,
isLoading: false,
designs: {
edges: [],
pageInfo: null,
},
}
},
created() {
this.fetchDesigns();
},
methods: {
handleError(error) {
this.isLoading = false;
// Do something with `error`
},
fetchDesigns(endCursor) {
this.isLoading = true;
return this.$apollo
.query({
query: projectQuery,
variables() {
return {
// ... The rest of the design variables
first: 10,
endCursor,
};
},
})
.then(({ data }) => {
const { id = null, issue = {} } = data.project || {};
const { edges = [], pageInfo } = issue.designCollection?.designs || {};
// Update data
this.designs = {
id,
edges: [...this.designs.edges, ...edges];
pageInfo: pageInfo;
};
// Increment the request count with each new result
this.requestCount += 1;
// Only fetch next page if we have more requests and there is a next page to fetch
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.hasNextPage) {
this.fetchDesigns(pageInfo.endCursor);
} else {
this.isLoading = false;
}
})
.catch(this.handleError);
},
},
```
### Managing performance
@ -666,34 +765,51 @@ it('calls mutation on submitting form ', () => {
To test the logic of Apollo cache updates, we might want to mock an Apollo Client in our unit tests. We use [`mock-apollo-client`](https://www.npmjs.com/package/mock-apollo-client) library to mock Apollo client and [`createMockApollo` helper](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/frontend/helpers/mock_apollo_helper.js) we created on top of it.
To separate tests with mocked client from 'usual' unit tests, it's recommended to create an additional component factory. This way we only create Apollo Client instance when it's necessary:
To separate tests with mocked client from 'usual' unit tests, it's recommended to create an additional factory and pass the created `mockApollo` as an option to the `createComponent`-factory. This way we only create Apollo Client instance when it's necessary.
```javascript
function createComponent() {...}
function createComponentWithApollo() {...}
```
Then we need to inject `VueApollo` to Vue local instance (`localVue.use()` can also be called within `createComponentWithApollo()`)
We need to inject `VueApollo` to the Vue local instance and, likewise, it is recommended to call `localVue.use()` within `createMockApolloProvider()` to only load it when it is necessary.
```javascript
import VueApollo from 'vue-apollo';
import { createLocalVue } from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(VueApollo);
function createMockApolloProvider() {
localVue.use(VueApollo);
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { mockApollo } = options;
...
return shallowMount(..., {
localVue,
apolloProvider: mockApollo,
...
});
}
```
After this, on the global `describe`, we should create a variable for `fakeApollo`:
After this, you can control whether you need a variable for `mockApollo` and assign it in the appropriate `describe`-scope:
```javascript
describe('Some component with Apollo mock', () => {
describe('Some component', () => {
let wrapper;
let fakeApollo
})
describe('with Apollo mock', () => {
let mockApollo;
beforeEach(() => {
mockApollo = createMockApolloProvider();
wrapper = createComponent({ mockApollo });
});
});
});
```
Within component factory, we need to define an array of _handlers_ for every query or mutation:
Within `createMockApolloProvider`-factory, we need to define an array of _handlers_ for every query or mutation:
```javascript
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
@ -702,13 +818,16 @@ import moveDesignMutation from '~/design_management/graphql/mutations/move_desig
describe('Some component with Apollo mock', () => {
let wrapper;
let fakeApollo;
let mockApollo;
function createMockApolloProvider() {
Vue.use(VueApollo);
function createComponentWithApollo() {
const requestHandlers = [
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
];
...
}
})
```
@ -718,23 +837,38 @@ After this, we need to create a mock Apollo Client instance using a helper:
```javascript
import createMockApollo from 'jest/helpers/mock_apollo_helper';
describe('Some component with Apollo mock', () => {
describe('Some component', () => {
let wrapper;
let fakeApollo;
function createComponentWithApollo() {
function createMockApolloProvider() {
Vue.use(VueApollo);
const requestHandlers = [
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
];
fakeApollo = createMockApollo(requestHandlers);
wrapper = shallowMount(Index, {
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { mockApollo } = options;
return shallowMount(Index, {
localVue,
apolloProvider: fakeApollo,
apolloProvider: mockApollo,
});
}
})
describe('with Apollo mock', () => {
let mockApollo;
beforeEach(() => {
mockApollo = createMockApolloProvider();
wrapper = createComponent({ mockApollo });
});
});
});
```
When mocking resolved values, ensure the structure of the response is the same
@ -744,13 +878,15 @@ When testing queries, please keep in mind they are promises, so they need to be
```javascript
it('renders a loading state', () => {
createComponentWithApollo();
const mockApollo = createMockApolloProvider();
const wrapper = createComponent({ mockApollo });
expect(wrapper.find(LoadingSpinner).exists()).toBe(true)
});
it('renders designs list', async () => {
createComponentWithApollo();
const mockApollo = createMockApolloProvider();
const wrapper = createComponent({ mockApollo });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@ -762,7 +898,7 @@ it('renders designs list', async () => {
If we need to test a query error, we need to mock a rejected value as request handler:
```javascript
function createComponentWithApollo() {
function createMockApolloProvider() {
...
const requestHandlers = [
[getDesignListQuery, jest.fn().mockRejectedValue(new Error('GraphQL error')],
@ -772,7 +908,7 @@ function createComponentWithApollo() {
...
it('renders error if query fails', async () => {
createComponent()
const wrapper = createComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@ -786,9 +922,11 @@ Request handlers can also be passed to component factory as a parameter.
Mutations could be tested the same way with a few additional `nextTick`s to get the updated result:
```javascript
function createComponentWithApollo({
function createMockApolloProvider({
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
}) {
Vue.use(VueApollo);
moveDesignHandler = moveHandler;
const requestHandlers = [
@ -797,15 +935,21 @@ function createComponentWithApollo({
[moveDesignMutation, moveDesignHandler],
];
fakeApollo = createMockApollo(requestHandlers);
wrapper = shallowMount(Index, {
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { mockApollo } = options;
return shallowMount(Index, {
localVue,
apolloProvider: fakeApollo,
apolloProvider: mockApollo,
});
}
...
it('calls a mutation with correct parameters and reorders designs', async () => {
createComponentWithApollo({});
const mockApollo = createMockApolloProvider({});
const wrapper = createComponent({ mockApollo });
wrapper.find(VueDraggable).vm.$emit('change', {
moved: {
@ -833,7 +977,7 @@ If your application contains `@client` queries, most probably you will have an A
```javascript
import createMockApollo from 'jest/helpers/mock_apollo_helper';
...
mockApollo = createMockApollo(requestHandlers, resolvers);
const mockApollo = createMockApollo(requestHandlers, resolvers);
```
Sometimes we want to test a `result` hook of the local query. In order to have it triggered, we need to populate a cache with correct data to be fetched with this query:
@ -850,12 +994,14 @@ query fetchLocalUser {
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
function createMockApolloProvider() {
Vue.use(VueApollo);
const requestHandlers = [
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
];
mockApollo = createMockApollo(requestHandlers, {});
const mockApollo = createMockApollo(requestHandlers, {});
mockApollo.clients.defaultClient.cache.writeQuery({
query: fetchLocalUserQuery,
data: {
@ -885,9 +1031,10 @@ Sometimes it is necessary to control what the local resolver returns and inspect
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
function createMockApolloProvider(options = {}) {
Vue.use(VueApollo);
const { fetchLocalUserSpy } = options;
mockApollo = createMockApollo([], {
const mockApollo = createMockApollo([], {
Query: {
fetchLocalUser: fetchLocalUserSpy,
},

View File

@ -485,17 +485,19 @@ request, be sure to start the `dont-interrupt-me` job before pushing.
1. All jobs must only pull caches by default.
1. All jobs must be able to pass with an empty cache. In other words, caches are only there to speed up jobs.
1. We currently have 6 different caches defined in
1. We currently have several different caches defined in
[`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml),
with fixed keys:
- `.rails-cache`.
- `.static-analysis-cache`.
- `.coverage-cache`
- `.qa-cache`
- `.yarn-cache`.
- `.assets-compile-cache` (the key includes `${NODE_ENV}` so it's actually two different caches).
1. Only 6 specific jobs, running in 2-hourly scheduled pipelines, are pushing (i.e. updating) to the caches:
- `update-rails-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
- `update-static-analysis-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
- `update-coverage-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
- `update-qa-cache`, defined in [`.gitlab/ci/qa.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/qa.gitlab-ci.yml).
- `update-assets-compile-production-cache`, defined in [`.gitlab/ci/frontend.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/frontend.gitlab-ci.yml).
- `update-assets-compile-test-cache`, defined in [`.gitlab/ci/frontend.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/frontend.gitlab-ci.yml).
@ -519,7 +521,7 @@ variable:
```shell
echo "Downloading archived master..."
wget -O /tmp/gitlab.tar.gz https://storage.googleapis.com/gitlab-ci-git-repo-cache/project-278964/gitlab-master.tar.gz
wget -O /tmp/gitlab.tar.gz https://storage.googleapis.com/gitlab-ci-git-repo-cache/project-278964/gitlab-master-shallow.tar.gz
if [ ! -f /tmp/gitlab.tar.gz ]; then
echo "Repository cache not available, cloning a new directory..."

View File

@ -268,6 +268,10 @@ module Gitlab
def has_database_scoped_labels?(current_mr_labels)
current_mr_labels.any? { |label| label.start_with?('database::') }
end
def has_ci_changes?
changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
end
end
end
end

View File

@ -14,6 +14,7 @@ class GitlabDanger
product_analytics
utility_css
pajamas
pipeline
].freeze
CI_ONLY_RULES ||= %w[

View File

@ -45,7 +45,7 @@ module QA
# Note: This test doesn't have the :orchestrated tag because it runs in the Test::Integration::Praefect
# scenario with other tests that aren't considered orchestrated.
# It also runs on staging using nfs-file07 as non-cluster storage and nfs-file22 as cluster/praefect storage
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974' do
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/284645', type: :investigating } do
let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } }
let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } }
let(:project) do

View File

@ -72,7 +72,7 @@ RSpec.describe Ci::PipelinesFinder do
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
end
it 'filters out child pipelines and show only the parents' do
it 'filters out child pipelines and shows only the parents by default' do
is_expected.to eq([parent_pipeline])
end
end
@ -195,6 +195,21 @@ RSpec.describe Ci::PipelinesFinder do
end
end
context 'when iids filter is specified' do
let(:params) { { iids: [pipeline1.iid, pipeline3.iid] } }
let!(:pipeline1) { create(:ci_pipeline, project: project) }
let!(:pipeline2) { create(:ci_pipeline, project: project) }
let!(:pipeline3) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
it 'returns matches pipelines' do
is_expected.to match_array([pipeline1, pipeline3])
end
it 'does not fitler out child pipelines' do
is_expected.to include(pipeline3)
end
end
context 'when sha is specified' do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: '97de212e80737a608d939f648d959671fb0a0142') }

View File

@ -1,6 +1,7 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import getters from 'ee_else_ce/boards/stores/getters';
@ -10,6 +11,11 @@ import BoardContent from '~/boards/components/board_content.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
const actions = {
moveList: jest.fn(),
showPromotionList: jest.fn(),
};
describe('BoardContent', () => {
let wrapper;
@ -21,12 +27,13 @@ describe('BoardContent', () => {
const createStore = (state = defaultState) => {
return new Vuex.Store({
actions,
getters,
state,
});
};
const createComponent = state => {
const createComponent = ({ state, props = {}, graphqlBoardListsEnabled = false } = {}) => {
const store = createStore({
...defaultState,
...state,
@ -37,25 +44,61 @@ describe('BoardContent', () => {
lists: mockListsWithModel,
canAdminList: true,
disabled: false,
...props,
},
provide: {
glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled },
},
store,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a BoardColumn component per list', () => {
createComponent();
expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
});
it('does not display EpicsSwimlanes component', () => {
createComponent();
expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
describe('graphqlBoardLists feature flag enabled', () => {
describe('can admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } });
});
it('renders draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(true);
});
});
describe('can not admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } });
});
it('renders draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(false);
});
});
});
describe('graphqlBoardLists feature flag disabled', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: false });
});
it('does not render draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(false);
});
});
});

View File

@ -290,6 +290,33 @@ describe('moveList', () => {
done,
);
});
it('should not commit MOVE_LIST or dispatch updateList if listId and replacedListId are the same', () => {
const initialBoardListsState = {
'gid://gitlab/List/1': mockListsWithModel[0],
'gid://gitlab/List/2': mockListsWithModel[1],
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: initialBoardListsState,
};
testAction(
actions.moveList,
{
listId: 'gid://gitlab/List/1',
replacedListId: 'gid://gitlab/List/1',
newIndex: 1,
adjustmentValue: 1,
},
state,
[],
[],
);
});
});
describe('updateList', () => {

View File

@ -18,6 +18,10 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
resolve(described_class, obj: project, args: args, ctx: { current_user: current_user })
end
before do
project.add_developer(current_user)
end
it 'resolves pipeline for the passed iid' do
result = batch_sync do
resolve_pipeline(project, { iid: '1234' })
@ -26,6 +30,21 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
expect(result).to eq(pipeline)
end
it 'keeps the queries under the threshold' do
create(:ci_pipeline, project: project, iid: '1235')
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
end
expect do
batch_sync do
resolve_pipeline(project, { iid: '1234' })
resolve_pipeline(project, { iid: '1235' })
end
end.not_to exceed_query_limit(control)
end
it 'does not resolve a pipeline outside the project' do
result = batch_sync do
resolve_pipeline(other_pipeline.project, { iid: '1234' })

View File

@ -591,4 +591,30 @@ RSpec.describe Gitlab::Danger::Helper do
expect(helper.prepare_labels_for_mr([])).to eq('')
end
end
describe '#has_ci_changes?' do
context 'when .gitlab/ci is changed' do
it 'returns true' do
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab/ci/test.yml])
expect(helper.has_ci_changes?).to be_truthy
end
end
context 'when .gitlab-ci.yml is changed' do
it 'returns true' do
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab-ci.yml])
expect(helper.has_ci_changes?).to be_truthy
end
end
context 'when neither .gitlab/ci/ or .gitlab-ci.yml is changed' do
it 'returns false' do
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb nested/.gitlab-ci.yml])
expect(helper.has_ci_changes?).to be_falsey
end
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
RSpec.describe GitlabDanger do
let(:gitlab_danger_helper) { nil }
@ -9,7 +9,7 @@ RSpec.describe GitlabDanger do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, product_analytics, utility_css, pajamas')
expect(described_class.local_warning_message).to eq("==> Only the following Danger rules can be run locally: #{described_class::LOCAL_RULES.join(', ')}")
end
end

View File

@ -5,12 +5,12 @@ require 'spec_helper'
RSpec.describe 'getting pipeline information nested in a project' do
include GraphqlHelpers
let(:project) { create(:project, :repository, :public) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:current_user) { create(:user) }
let!(:project) { create(:project, :repository, :public) }
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:current_user) { create(:user) }
let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] }
let(:query) do
let!(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
@ -35,4 +35,45 @@ RSpec.describe 'getting pipeline information nested in a project' do
expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE')
end
context 'batching' do
let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
it 'executes the finder once' do
mock = double(Ci::PipelinesFinder)
opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
expect(Ci::PipelinesFinder).to receive(:new).once.with(project, current_user, opts).and_return(mock)
expect(mock).to receive(:execute).once.and_return(Ci::Pipeline.none)
post_graphql(query, current_user: current_user)
end
it 'keeps the queries under the threshold' do
control = ActiveRecord::QueryRecorder.new do
single_pipeline_query = build_query_to_find_pipeline_shas(pipeline)
post_graphql(single_pipeline_query, current_user: current_user)
end
aggregate_failures do
expect(response).to have_gitlab_http_status(:success)
expect do
post_graphql(query, current_user: current_user)
end.not_to exceed_query_limit(control)
end
end
end
private
def build_query_to_find_pipeline_shas(*pipelines)
pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
"pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
end.join(' ')
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
end
end

View File

@ -50,13 +50,13 @@ RSpec.describe Tooling::TestMapGenerator do
let(:expected_mapping) do
{
'lib/gitlab/current_settings.rb' => [
'./spec/factories_spec.rb'
'spec/factories_spec.rb'
],
'lib/feature.rb' => [
'./spec/factories_spec.rb'
'spec/factories_spec.rb'
],
'lib/gitlab/marginalia.rb' => [
'./spec/factories_spec.rb'
'spec/factories_spec.rb'
]
}
end
@ -80,16 +80,16 @@ RSpec.describe Tooling::TestMapGenerator do
let(:expected_mapping) do
{
'lib/gitlab/current_settings.rb' => [
'./spec/factories_spec.rb',
'./spec/models/project_spec.rb'
'spec/factories_spec.rb',
'spec/models/project_spec.rb'
],
'lib/feature.rb' => [
'./spec/factories_spec.rb',
'./spec/models/project_spec.rb'
'spec/factories_spec.rb',
'spec/models/project_spec.rb'
],
'lib/gitlab/marginalia.rb' => [
'./spec/factories_spec.rb',
'./spec/models/project_spec.rb'
'spec/factories_spec.rb',
'spec/models/project_spec.rb'
]
}
end

View File

@ -17,7 +17,7 @@ module Tooling
example_groups.each do |example_id, files|
files.each do |file|
spec_file = strip_example_uid(example_id)
@mapping[file] << spec_file
@mapping[file] << spec_file.delete_prefix('./')
end
end
end