Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-15 18:10:06 +00:00
parent 0ff373dc41
commit b07852468f
56 changed files with 735 additions and 295 deletions

View File

@ -1,8 +1,11 @@
import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import { STATUSES } from '../../constants';
import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
import { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller';
export const clientTypenames = {
BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
@ -10,6 +13,8 @@ export const clientTypenames = {
};
export function createResolvers({ endpoints }) {
let statusPoller;
return {
Query: {
async bulkImportSourceGroups(_, __, { client }) {
@ -57,6 +62,30 @@ export function createResolvers({ endpoints }) {
const groupManager = new SourceGroupsManager({ client });
const group = groupManager.findById(sourceGroupId);
groupManager.setImportStatus(group, STATUSES.SCHEDULING);
try {
await axios.post(endpoints.createBulkImport, {
bulk_import: [
{
source_type: 'group_entity',
source_full_path: group.full_path,
destination_namespace: group.import_target.target_namespace,
destination_name: group.import_target.new_name,
},
],
});
groupManager.setImportStatus(group, STATUSES.STARTED);
if (!statusPoller) {
statusPoller = new StatusPoller({ client, interval: 3000 });
statusPoller.startPolling();
}
} catch (e) {
createFlash({
message: s__('BulkImport|Importing the group failed'),
});
groupManager.setImportStatus(group, STATUSES.NONE);
throw e;
}
},
},
};

View File

@ -0,0 +1,68 @@
import gql from 'graphql-tag';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import bulkImportSourceGroupsQuery from '../queries/bulk_import_source_groups.query.graphql';
import { STATUSES } from '../../../constants';
import { SourceGroupsManager } from './source_groups_manager';
const groupId = i => `group${i}`;
function generateGroupsQuery(groups) {
return gql`{
${groups
.map(
(g, idx) =>
`${groupId(idx)}: group(fullPath: "${g.import_target.target_namespace}/${
g.import_target.new_name
}") { id }`,
)
.join('\n')}
}`;
}
export class StatusPoller {
constructor({ client, interval }) {
this.client = client;
this.interval = interval;
this.timeoutId = null;
this.groupManager = new SourceGroupsManager({ client });
}
startPolling() {
if (this.timeoutId) {
return;
}
this.checkPendingImports();
}
stopPolling() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
async checkPendingImports() {
try {
const { bulkImportSourceGroups } = this.client.readQuery({
query: bulkImportSourceGroupsQuery,
});
const groupsInProgress = bulkImportSourceGroups.filter(g => g.status === STATUSES.STARTED);
if (groupsInProgress.length) {
const { data: results } = await this.client.query({
query: generateGroupsQuery(groupsInProgress),
fetchPolicy: 'no-cache',
});
const completedGroups = groupsInProgress.filter((_, idx) => Boolean(results[groupId(idx)]));
completedGroups.forEach(group => {
this.groupManager.setImportStatus(group, STATUSES.FINISHED);
});
}
} catch (e) {
createFlash({
message: s__('BulkImport|Update of import statuses with realtime changes failed'),
});
} finally {
this.timeoutId = setTimeout(() => this.checkPendingImports(), this.interval);
}
}
}

View File

@ -2,6 +2,7 @@
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/216102
export const BACKSPACE_KEY_CODE = 8;
export const TAB_KEY_CODE = 9;
export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27;
export const UP_KEY_CODE = 38;

View File

@ -1,11 +1,11 @@
#import "~/pipelines/graphql/queries/pipeline_stages.fragment.graphql"
#import "~/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql"
query getCiConfigData($content: String!) {
ciConfig(content: $content) {
errors
status
stages {
...PipelineStagesData
...PipelineStagesConnection
}
}
}

View File

@ -10,6 +10,7 @@ import TextEditor from './components/text_editor.vue';
import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
@ -99,7 +100,11 @@ export default {
};
},
update(data) {
return data?.ciConfig ?? {};
const { ciConfigData } = data || {};
const stageNodes = ciConfigData?.stages?.nodes || [];
const stages = unwrapStagesWithNeeds(stageNodes);
return { ...ciConfigData, stages };
},
error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN);

View File

@ -1,12 +0,0 @@
fragment PipelineStagesData on CiConfigStage {
name
groups {
name
jobs {
name
needs {
name
}
}
}
}

View File

@ -0,0 +1,20 @@
fragment PipelineStagesConnection on CiConfigStageConnection {
nodes {
name
groups {
nodes {
name
jobs {
nodes {
name
needs {
nodes {
name
}
}
}
}
}
}
}
}

View File

@ -1,5 +1,5 @@
<script>
import Tribute from 'tributejs';
import Tribute from '@gitlab/tributejs';
import {
GfmAutocompleteType,
tributeConfig,
@ -29,6 +29,10 @@ export default {
config() {
return this.autocompleteTypes.map(type => ({
...tributeConfig[type].config,
loadingItemTemplate: `<span class="gl-spinner gl-vertical-align-text-bottom gl-ml-3 gl-mr-2"></span>${__(
'Loading',
)}`,
requireLeadingSpace: true,
values: this.getValues(type),
}));
},

View File

@ -143,7 +143,7 @@ module Ci
def specified_cross_pipeline_dependencies
strong_memoize(:specified_cross_pipeline_dependencies) do
next [] unless Feature.enabled?(:ci_cross_pipeline_artifacts_download, processable.project, default_enabled: false)
next [] unless Feature.enabled?(:ci_cross_pipeline_artifacts_download, processable.project, default_enabled: true)
specified_cross_dependencies.select { |dep| dep[:pipeline] && dep[:artifacts] }
end

View File

@ -1493,6 +1493,10 @@ class User < ApplicationRecord
!solo_owned_groups.present?
end
def can_remove_self?
true
end
def ci_owned_runners
@ci_owned_runners ||= begin
project_runners = Ci::RunnerProject

View File

@ -18,14 +18,19 @@ module Jira
request
end
# We have to add the context_path here because the Jira client is not taking it into account
def base_api_url
"/rest/api/#{api_version}"
"#{context_path}/rest/api/#{api_version}"
end
private
attr_reader :jira_service, :project
def context_path
client.options[:context_path].to_s
end
# override this method in the specific request class implementation if a differnt API version is required
def api_version
JIRA_API_VERSION

View File

@ -3,9 +3,9 @@
- breadcrumb_title _('Import groups')
%h1.gl-my-0.gl-py-4.gl-font-size-h1.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1
= s_('ImportGroups|Import groups from GitLab')
= s_('BulkImport|Import groups from GitLab')
%p.gl-my-0.gl-py-5.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1
= s_('ImportGroups|Importing groups from %{link}').html_safe % { link: external_link(@source_url, @source_url) }
= s_('BulkImport|Importing groups from %{link}').html_safe % { link: external_link(@source_url, @source_url) }
#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json),
available_namespaces_path: import_available_namespaces_path(format: :json),

View File

@ -79,6 +79,11 @@
%strong= current_user.solo_owned_groups.map(&:name).join(', ')
%p
= s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.')
- elsif !current_user.can_remove_self?
%p
= s_('Profiles|GitLab is unable to verify your identity automatically.')
%p
= s_('Profiles|Please email %{data_request} to begin the account deletion process.').html_safe % { data_request: mail_to('personal-data-request@gitlab.com') }
- else
%p
= s_("Profiles|You don't have access to delete this user.")

View File

@ -0,0 +1,5 @@
---
title: Add expires_at param to GroupMemberBuilder data
merge_request: 49981
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Allow job to download artifacts in parent-child pipeline hierarchy
merge_request: 49837
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Introduce frontend for group migration MVC
merge_request: 49709
author:
type: added

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/287622
milestone: '13.7'
type: development
group: group::continuous integration
default_enabled: false
default_enabled: true

View File

@ -16,7 +16,7 @@ module ActiveRecord
options[:null] = false if options[:null].nil?
[:created_at, :updated_at].each do |column_name|
column(column_name, :datetime_with_timezone, options)
column(column_name, :datetime_with_timezone, **options)
end
end
@ -27,7 +27,7 @@ module ActiveRecord
# t.datetime_with_timezone :did_something_at
# end
def datetime_with_timezone(column_name, **options)
column(column_name, :datetime_with_timezone, options)
column(column_name, :datetime_with_timezone, **options)
end
# Disable timestamp alias to datetime

View File

@ -22,11 +22,15 @@ In the Gitaly documentation:
GitLab end users do not have direct access to Gitaly. Gitaly only manages Git
repository access for GitLab. Other types of GitLab data aren't accessed using Gitaly.
<!-- vale gitlab.FutureTense = NO -->
WARNING:
From GitLab 13.0, Gitaly support for NFS is deprecated. As of GitLab 14.0, NFS-related issues
with Gitaly will no longer be addressed. Upgrade to [Gitaly Cluster](praefect.md) as soon as
possible. Watch for [tools to enable bulk move](https://gitlab.com/groups/gitlab-org/-/epics/4916)
of projects to Gitaly Cluster.
possible. Tools to [enable bulk moves](https://gitlab.com/groups/gitlab-org/-/epics/4916)
of projects to Gitaly Cluster are planned.
<!-- vale gitlab.FutureTense = YES -->
## Architecture

View File

@ -27,7 +27,7 @@ In brief:
- When a user navigates to the terminal page for an environment, they are served
a JavaScript application that opens a WebSocket connection back to GitLab.
- The WebSocket is handled in [Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse),
rather than the Rails application server.
rather than the Rails application server.
- Workhorse queries Rails for connection details and user permissions. Rails
queries Kubernetes for them in the background using [Sidekiq](../troubleshooting/sidekiq.md).
- Workhorse acts as a proxy server between the user's browser and the Kubernetes
@ -44,7 +44,7 @@ everything protected with authorization guards. This is described in more
detail below.
- Interactive web terminals are completely disabled unless [`[session_server]`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) is configured.
- Every time the runner starts, it will generate an `x509` certificate that will be used for a `wss` (Web Socket Secure) connection.
- Every time the runner starts, it generates an `x509` certificate that is used for a `wss` (Web Socket Secure) connection.
- For every created job, a random URL is generated which is discarded at the end of the job. This URL is used to establish a web socket connection. The URL for the session is in the format `(IP|HOST):PORT/session/$SOME_HASH`, where the `IP/HOST` and `PORT` are the configured [`listen_address`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section).
- Every session URL that is created has an authorization header that needs to be sent, to establish a `wss` connection.
- The session URL is not exposed to the users in any way. GitLab holds all the state internally and proxies accordingly.
@ -72,7 +72,7 @@ guides document the necessary steps for a selection of popular reverse proxies:
- [HAProxy](https://www.haproxy.com/blog/websockets-load-balancing-with-haproxy/)
- [Varnish](https://varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html)
Workhorse won't let WebSocket requests through to non-WebSocket endpoints, so
Workhorse doesn't let WebSocket requests through to non-WebSocket endpoints, so
it's safe to enable support for these headers globally. If you'd rather had a
narrower set of rules, you can restrict it to URLs ending with `/terminal.ws`
(although this may still have a few false positives).
@ -85,7 +85,7 @@ document for more details.
If you'd like to disable web terminal support in GitLab, just stop passing
the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse
proxy in the chain. For most users, this will be the NGINX server bundled with
proxy in the chain. For most users, this is the NGINX server bundled with
Omnibus GitLab, in which case, you need to:
- Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file
@ -95,9 +95,9 @@ Omnibus GitLab, in which case, you need to:
For your own load balancer, just reverse the configuration changes recommended
by the above guides.
When these headers are not passed through, Workhorse will return a
When these headers are not passed through, Workhorse returns a
`400 Bad Request` response to users attempting to use a web terminal. In turn,
they will receive a `Connection failed` message.
they receive a `Connection failed` message.
## Limiting WebSocket connection time

View File

@ -68,6 +68,11 @@ The following reference architectures are available:
- [Up to 25,000 users](25k_users.md)
- [Up to 50,000 users](50k_users.md)
A GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/#self-managed) license is required
to get assistance from Support with troubleshooting the [2,000 users](2k_users.md)
and higher reference architectures.
[Read more about our definition of scaled architectures](https://about.gitlab.com/support/#definition-of-scaled-architecture).
## Availability Components
GitLab comes with the following components for your use, listed from least to

View File

@ -374,6 +374,26 @@ Clear the cache:
sudo gitlab-rake cache:clear
```
### Export a repository
It's typically recommended to export a project through [the web interface](../../user/project/settings/import_export.md#exporting-a-project-and-its-data) or through [the API](../../api/project_import_export.md). In situations where this is not working as expected, it may be preferable to export a project directly via the Rails console:
```ruby
user = User.find_by_username('USERNAME')
project = Project.find_by_full_path('PROJECT_PATH')
Projects::ImportExport::ExportService.new(project, user).execute
```
If the project you wish to export is available at `https://gitlab.example.com/baltig/pipeline-templates`, the value to use for `PROJECT_PATH` would be `baltig/pipeline-templates`.
If this all runs successfully, you will see output like the following before being returned to the Rails console prompt:
```ruby
=> nil
```
The exported project will be located within a `.tar.gz` file in `/var/opt/gitlab/gitlab-rails/uploads/-/system/import_export_upload/export_file/`.
## Repository
### Search sequence of pushes to a repository

View File

@ -8383,7 +8383,7 @@ type EpicIssue implements CurrentUserTodos & Noteable {
epicIssueId: ID!
"""
Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.
Current health status.
"""
healthStatus: HealthStatus
@ -11154,7 +11154,7 @@ type Issue implements CurrentUserTodos & Noteable {
epic: Epic
"""
Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.
Current health status.
"""
healthStatus: HealthStatus

View File

@ -23462,7 +23462,7 @@
},
{
"name": "healthStatus",
"description": "Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.",
"description": "Current health status.",
"args": [
],
@ -30725,7 +30725,7 @@
},
{
"name": "healthStatus",
"description": "Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.",
"description": "Current health status.",
"args": [
],

View File

@ -1435,7 +1435,7 @@ Relationship between an epic and an issue.
| `emailsDisabled` | Boolean! | Indicates if a project has email notifications disabled: `true` if email notifications are disabled |
| `epic` | Epic | Epic to which this issue belongs. |
| `epicIssueId` | ID! | ID of the epic-issue relation |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `healthStatus` | HealthStatus | Current health status. |
| `humanTimeEstimate` | String | Human-readable time estimate of the issue |
| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue |
| `id` | ID | Global ID of the epic-issue relation |
@ -1751,7 +1751,7 @@ Represents a recorded measurement (object count) for the Admins.
| `dueDate` | Time | Due date of the issue |
| `emailsDisabled` | Boolean! | Indicates if a project has email notifications disabled: `true` if email notifications are disabled |
| `epic` | Epic | Epic to which this issue belongs. |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `healthStatus` | HealthStatus | Current health status. |
| `humanTimeEstimate` | String | Human-readable time estimate of the issue |
| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue |
| `id` | ID! | ID of the issue |

View File

@ -2088,12 +2088,59 @@ build_job:
needs:
- project: $CI_PROJECT_PATH
job: $DEPENDENCY_JOB_NAME
ref: $CI_COMMIT_BRANCH
ref: $ARTIFACTS_DOWNLOAD_REF
artifacts: true
```
Downloading artifacts from jobs that are run in [`parallel:`](#parallel) is not supported.
To download artifacts between [parent-child pipelines](../parent_child_pipelines.md) use [`needs:pipeline`](#artifact-downloads-to-child-pipelines).
Downloading artifacts from the same ref as the currently running pipeline is not
recommended because artifacts could be overridden by concurrent pipelines running
on the same ref.
##### Artifact downloads to child pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255983) in GitLab v13.7.
A [child pipeline](../parent_child_pipelines.md) can download artifacts from a job in
its parent pipeline or another child pipeline in the same parent-child pipeline hierarchy.
For example, with the following parent pipeline that has a job that creates some artifacts:
```yaml
create-artifact:
stage: build
script: echo 'sample artifact' > artifact.txt
artifacts:
paths: [artifact.txt]
child-pipeline:
stage: test
trigger:
include: child.yml
strategy: depend
variables:
PARENT_PIPELINE_ID: $CI_PIPELINE_ID
```
A job in the child pipeline can download artifacts from the `create-artifact` job in
the parent pipeline:
```yaml
use-artifact:
script: cat artifact.txt
needs:
- pipeline: $PARENT_PIPELINE_ID
job: create-artifact
```
The `pipeline` attribute accepts a pipeline ID and it must be a pipeline present
in the same parent-child pipeline hierarchy of the given pipeline.
The `pipeline` attribute does not accept the current pipeline ID (`$CI_PIPELINE_ID`).
To download artifacts from a job in the current pipeline, use the basic form of [`needs`](#artifact-downloads-with-needs).
### `tags`
Use `tags` to select a specific runner from the list of all runners that are

View File

@ -48,7 +48,7 @@ If new migrations are introduced, in the MR **you are required to provide**:
If new queries have been introduced or existing queries have been updated, **you are required to provide**:
- [Query plans](#query-plans) for each raw SQL query included in the merge request along with the link to the query plan following each raw SQL snippet.
- [Raw SQL](#raw-sql) for all queries (as translated from ActiveRecord queries).
- [Raw SQL](#raw-sql) for all changed or added queries (as translated from ActiveRecord queries).
- In case of updating an existing query, the raw SQL of both the old and the new version of the query should be provided together with their query plans.
Refer to [Preparation when adding or modifying queries](#preparation-when-adding-or-modifying-queries) for how to provide this information.

View File

@ -526,7 +526,7 @@ You can use the following fake tokens as examples:
### Usage list
<!-- vale off -->
| {::nomarkdown}<div style="width:140px">Usage</div>{:/} | Guidance |
| Usage | Guidance |
|-----------------------|-----|
| and/or | Use **or** instead, or another sensible construction. |
| currently | Do not use when talking about the product or its features. The documentation describes the product as it is today. |

View File

@ -63,7 +63,7 @@ and you can [customize the payload](#customize-the-alert-payload-outside-of-gitl
for the webhook configuration. You must also input the URL and Authorization Key
in your external service.
1. _(Optional)_ To generate a test alert to test the new integration, enter a
sample payload, then click **Save and test alert payload**.Valid JSON is required.
sample payload, then click **Save and test alert payload**. Valid JSON is required.
1. Click **Save Integration**.
The new HTTP Endpoint displays in the [integrations list](#integrations-list).

View File

@ -50,7 +50,7 @@ The DevOps Adoption tab shows you which segments of your organization are using
- Deploys
- Scanning
Segments are arbitrary collections of GitLab groups and projects that you define. You might define a segment to represent a small team, a large department, or a whole organization. You are limited to creating a maximum of 20 segments, and each segment is limited to a maximum of 20 groups. Buttons to manage your segments appear in the DevOps Adoption section of the page.
Segments are arbitrary collections of GitLab groups that you define. You might define a segment to represent a small team, a large department, or a whole organization. You are limited to creating a maximum of 20 segments, and each segment is limited to a maximum of 20 groups. Buttons to manage your segments appear in the DevOps Adoption section of the page.
DevOps Adoption allows you to:

View File

@ -82,6 +82,41 @@ are marked with `"throttle_safelist":"throttle_user_allowlist"` in
At application startup, the allowlist is logged in [`auth.log`](../../../administration/logs.md#authlog).
## Trying out throttling settings before enforcing them
> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/629) in GitLab 13.6.
Trying out throttling settings can be done by setting the
`GITLAB_THROTTLE_DRY_RUN` environment variable to a comma-separated
list of throttle names.
The possible names are:
- `throttle_unauthenticated`
- `throttle_authenticated_api`
- `throttle_authenticated_web`
- `throttle_unauthenticated_protected_paths`
- `throttle_authenticated_protected_paths_api`
- `throttle_authenticated_protected_paths_web`
For example: trying out throttles for all authenticated requests to
non-protected paths could be done by setting
`GITLAB_THROTTLE_DRY_RUN='throttle_authenticated_web,throttle_authenticated_api'`.
To enable the dry-run mode for all throttles, the variable can be set
to `*`.
Setting a throttle to dry-run mode will log a message to the
[`auth.log`](../../../administration/logs.md#authlog) when it would
hit the limit, while letting the request continue as normal. The log
message will contain an `env` field set to `track`. The `matched`
field will contain the name of throttle that was hit.
It is important to set the environment variable **before** enabling
the rate limiting in the settings. The settings in the admin panel
take effect immediately, while setting the environment variable
requires a restart of all the Puma processes.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -79,14 +79,10 @@ to:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/199184) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
> - The health status of a closed issue [is hidden](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3 or later.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7.
Report or respond to the health of issues and epics by setting a red, amber, or green [health status](../../project/issues/index.md#health-status), which then appears on your Epic tree.
### Disable Issue health status in Epic tree
This feature comes with a feature flag enabled by default. For steps to disable it, see
[Disable issue health status](../../project/issues/index.md#disable-issue-health-status).
## Multi-level child epics **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8333) in GitLab Ultimate 11.7.

View File

@ -94,4 +94,3 @@ The **Packages & Registries > Package Registry** entry is removed from the sideb
Learn how to use the GitLab Package Registry to build your own custom package workflow.
- [Use a project as a package registry](../workflows/project_registry.md) to publish all of your packages to one project.
- Publish multiple different packages from one [monorepo project](../workflows/monorepo.md).

View File

@ -1,120 +1,9 @@
---
stage: Package
group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
redirect_to: '../npm_registry/index.md'
disqus_identifier: 'https://docs.gitlab.com/ee/user/packages/workflows/monorepo.html'
---
# Monorepo package management workflows
This document was moved to [another location](../npm_registry/index.md).
Oftentimes, one project or Git repository may contain multiple different
sub-projects or submodules that all get packaged and published individually.
## Publishing different packages to the parent project
The number and name of packages you can publish to one project is not limited.
You can accomplish this by setting up different configuration files for each
package. See the documentation for the package manager of your choice since
each has its own specific files and instructions to follow to publish
a given package.
Here, we take a walk through how to do this with [NPM](../npm_registry/index.md).
Let us say we have a project structure like so:
```plaintext
MyProject/
|- src/
| |- components/
| |- Foo/
|- package.json
```
`MyProject` is the parent project, which contains a sub-project `Foo` in the
`components` directory. We would like to publish packages for both `MyProject`
as well as `Foo`.
Following the instructions in the
[GitLab NPM registry documentation](../npm_registry/index.md),
publishing `MyProject` consists of modifying the `package.json` file with a
`publishConfig` section, as well as either modifying your local NPM configuration with
CLI commands like `npm config set`, or saving a `.npmrc` file in the root of the
project specifying these configuration settings.
If you follow the instructions you can publish `MyProject` by running
`npm publish` from the root directory.
Publishing `Foo` is almost exactly the same, you simply have to follow the steps
while in the `Foo` directory. `Foo` needs its own `package.json` file,
which can be added manually or using `npm init`. It also needs its own
configuration settings. Since you are publishing to the same place, if you
used `npm config set` to set the registry for the parent project, then no
additional setup is necessary. If you used a `.npmrc` file, you need an
additional `.npmrc` file in the `Foo` directory (be sure to add `.npmrc` files
to the `.gitignore` file or use environment variables in place of your access
tokens to prevent them from being exposed). It can be identical to the
one you used in `MyProject`. You can now run `npm publish` from the `Foo`
directory and you can publish `Foo` separately from `MyProject`
A similar process could be followed for Conan packages, instead of dealing with
`.npmrc` and `package.json`, you just deal with `conanfile.py` in
multiple locations within the project.
## Publishing to other projects
A package is associated with a project on GitLab, but the package does not
need to be associated with the code in that project. Notice when configuring
NPM or Maven, you only use the `Project ID` to set the registry URL that the
package is to be uploaded to. If you set this to any project that you have
access to and update any other configuration similarly depending on the package type,
your packages are published to that project. This means you can publish
multiple packages to one project, even if their code does not exist in the same
place. See the [project registry workflow documentation](project_registry.md)
for more details.
## CI workflows for automating packaging
CI pipelines open an entire world of possibilities for dealing with the patterns
described in the previous sections. A common desire would be to publish
specific packages only if changes were made to those directories.
Using the example project above, this `gitlab-ci.yml` file publishes
`Foo` anytime changes are made to the `Foo` directory on the `master` branch,
and publish `MyPackage` anytime changes are made to anywhere _except_ the `Foo`
directory on the `master` branch.
```yaml
image: node:latest
stages:
- build
build-foo-package:
stage: build
variables:
PACKAGE: "Foo"
script:
- cd src/components/Foo
- echo "Building $PACKAGE"
- npm publish
only:
refs:
- master
- merge_requests
changes:
- "src/components/Foo/**/*"
build-my-project-package:
stage: build
variables:
PACKAGE: "MyPackage"
script:
- echo "Building $PACKAGE"
- npm publish
only:
refs:
- master
- merge_requests
except:
changes:
- "src/components/Foo/**/*"
```
<!-- This redirect file can be deleted after <2021-02-14>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1387,6 +1387,7 @@ X-Gitlab-Event: Member Hook
"user_id": 64,
"group_access": "Guest",
"group_plan": null,
"expires_at": "2020-12-14T00:00:00Z",
"event_name": "user_add_to_group"
}
```

View File

@ -4,7 +4,7 @@ group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Issues
# Issues **(CORE)**
Issues are the fundamental medium for collaborating on ideas and planning work in GitLab.
@ -191,6 +191,7 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4 and later.
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7.
To help you track the status of your issues, you can assign a status to each issue to flag work
that's progressing as planned or needs attention to keep on schedule:
@ -207,16 +208,6 @@ until the issue is reopened.
You can then see issue statuses in the [issue list](#issues-list) and the
[Epic tree](../../group/epics/index.md#issue-health-status-in-epic-tree).
#### Disable issue health status
This feature comes with the `:save_issuable_health_status` feature flag enabled by default. However, in some cases
this feature is incompatible with old configuration. To turn off the feature while configuration is
migrated, ask a GitLab administrator with Rails console access to run the following command:
```ruby
Feature.disable(:save_issuable_health_status)
```
## Other Issue actions
- [Create an issue from a template](../../project/description_templates.md#using-the-templates)

View File

@ -4,10 +4,21 @@ group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Sorting and ordering issue lists
# Sorting and ordering issue lists **(CORE)**
You can sort a list of issues several ways, including by issue creation date, milestone due date,
etc. The available sorting options can change based on the context of the list.
You can sort a list of issues several ways, including by:
- Blocking
- Created date
- Due date
- Label priority
- Last updated
- Milestone due date
- Popularity
- Priority
- Weight
The available sorting options can change based on the context of the list.
For sorting by issue priority, see [Label Priority](../labels.md#label-priority).
In group and project issue lists, it is also possible to order issues manually,
@ -18,19 +29,25 @@ similar to [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/62178) in GitLab 12.2.
When you select **Manual** sorting, you can change
the order by dragging and dropping the issues. The changed order will persist. Everyone who visits the same list will see the reordered list, with some exceptions.
the order by dragging and dropping the issues. The changed order persists, and
everyone who visits the same list sees the updated issue order, with some exceptions.
Each issue is assigned a relative order value, representing its relative
order with respect to the other issues in the list. When you drag-and-drop reorder
an issue, its relative order value changes accordingly.
order with respect to the other issues on the list. When you drag-and-drop reorder
an issue, its relative order value changes.
In addition, any time that issue appears in a manually sorted list,
the updated relative order value will be used for the ordering. This means that
if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
a given list inside your GitLab instance, any time those two issues are subsequently
loaded in any list in the same instance (could be a different project issue list or a
different group issue list, for example), that ordering will be maintained.
In addition, any time an issue appears in a manually sorted list,
the updated relative order value is used for the ordering.
So, if anyone drags issue `A` above issue `B` in your GitLab instance,
this ordering is maintained whenever they appear together in any list.
This ordering also affects [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
Changing the order in an issue list changes the ordering in an issue board,
and vice versa.
## Sorting by blocking issues
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34247/) in GitLab 13.7.
When you select to sort by **Blocking**, the issue list changes to sort descending by the
number of issues each issue is blocking. You can use this to determine the critical path for your backlog.

View File

@ -59,7 +59,7 @@ module Banzai
super(object_sym, tooltip: false)
end
def data_attributes_for(text, parent, object, data = {})
def data_attributes_for(text, parent, object, **data)
super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
end

View File

@ -19,6 +19,7 @@ module Gitlab
# :group_access=>"Guest",
# :created_at=>"2020-11-04T10:12:10Z",
# :updated_at=>"2020-11-04T10:12:10Z",
# :expires_at=>"2020-12-04T10:12:10Z"
# }
def build(event)
@ -40,7 +41,8 @@ module Gitlab
user_name: group_member.user.name,
user_email: group_member.user.email,
user_id: group_member.user.id,
group_access: group_member.human_access
group_access: group_member.human_access,
expires_at: group_member.expires_at&.xmlschema
}
end

View File

@ -5,8 +5,8 @@ module Gitlab
module Project
module Sample
class RelationTreeRestorer < ImportExport::RelationTreeRestorer
def initialize(*args)
super
def initialize(...)
super(...)
@date_calculator = Gitlab::ImportExport::Project::Sample::DateCalculator.new(dates)
end

View File

@ -155,7 +155,7 @@ module Gitlab
transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
end
relation = @relation_factory.create(relation_factory_params(relation_key, data_hash))
relation = @relation_factory.create(**relation_factory_params(relation_key, data_hash))
if relation && !relation.valid?
@shared.logger.warn(

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# When adding new user-configurable throttles, remember to update the documentation
# in doc/user/admin_area/settings/user_and_ip_rate_limits.md
#
# Integration specs for throttling can be found in:
# spec/requests/rack_attack_global_spec.rb
module Gitlab

View File

@ -67,7 +67,7 @@ module Gitlab
batch_size: 1000
}
relation.find_each(find_params) do |upload|
relation.find_each(**find_params) do |upload|
clean(upload.retrieve_uploader, dry_run: dry_run)
sleep sleep_time if sleep_time
rescue => err

View File

@ -192,11 +192,17 @@ namespace :gitlab do
exit
end
indexes = if args[:index_name]
[Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])]
else
Gitlab::Database::Reindexing.candidate_indexes
end
indexes = Gitlab::Database::Reindexing.candidate_indexes
if identifier = args[:index_name]
raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/
indexes = indexes.where(identifier: identifier)
raise "Index not found or not supported: #{args[:index_name]}" if indexes.empty?
end
ActiveRecord::Base.logger = Logger.new(STDOUT) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false)
Gitlab::Database::Reindexing.perform(indexes)
rescue => e

View File

@ -4777,9 +4777,21 @@ msgstr ""
msgid "BulkImport|From source group"
msgstr ""
msgid "BulkImport|Import groups from GitLab"
msgstr ""
msgid "BulkImport|Importing groups from %{link}"
msgstr ""
msgid "BulkImport|Importing the group failed"
msgstr ""
msgid "BulkImport|To new group"
msgstr ""
msgid "BulkImport|Update of import statuses with realtime changes failed"
msgstr ""
msgid "BulkImport|expected an associated Group but has an associated Project"
msgstr ""
@ -9421,9 +9433,6 @@ msgstr ""
msgid "Describe the goal of the changes and what reviewers should be aware of."
msgstr ""
msgid "Describe the requirement here"
msgstr ""
msgid "Description"
msgstr ""
@ -14476,12 +14485,6 @@ msgstr ""
msgid "ImportButtons|Connect repositories from"
msgstr ""
msgid "ImportGroups|Import groups from GitLab"
msgstr ""
msgid "ImportGroups|Importing groups from %{link}"
msgstr ""
msgid "ImportProjects|%{provider} rate limit exceeded. Try again later"
msgstr ""
@ -21088,6 +21091,9 @@ msgstr ""
msgid "Profiles|@username"
msgstr ""
msgid "Profiles|Account could not be deleted. GitLab was unable to verify your identity."
msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
@ -21190,6 +21196,9 @@ msgstr ""
msgid "Profiles|Full name"
msgstr ""
msgid "Profiles|GitLab is unable to verify your identity automatically."
msgstr ""
msgid "Profiles|Give your individual key a title."
msgstr ""
@ -21238,6 +21247,9 @@ msgstr ""
msgid "Profiles|Path"
msgstr ""
msgid "Profiles|Please email %{data_request} to begin the account deletion process."
msgstr ""
msgid "Profiles|Position and size your new avatar"
msgstr ""
@ -23567,9 +23579,6 @@ msgstr ""
msgid "Requirement %{reference} has been updated"
msgstr ""
msgid "Requirement title"
msgstr ""
msgid "Requirement title cannot have more than %{limit} characters."
msgstr ""

View File

@ -43,6 +43,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.177.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "24.8.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-3",
@ -139,7 +140,6 @@
"tiptap": "^1.8.0",
"tiptap-commands": "^1.4.0",
"tiptap-extensions": "^1.8.0",
"tributejs": "5.1.3",
"url-loader": "^3.0.0",
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",

View File

@ -37,6 +37,8 @@ FactoryBot.define do
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
group_runners_enabled { nil }
merge_pipelines_enabled { nil }
merge_trains_enabled { nil }
import_status { nil }
import_jid { nil }
import_correlation_id { nil }
@ -77,7 +79,9 @@ FactoryBot.define do
project.group&.refresh_members_authorized_projects
# assign the delegated `#ci_cd_settings` attributes after create
project.reload.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
project.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
project.merge_pipelines_enabled = evaluator.merge_pipelines_enabled unless evaluator.merge_pipelines_enabled.nil?
project.merge_trains_enabled = evaluator.merge_trains_enabled unless evaluator.merge_trains_enabled.nil?
if evaluator.import_status
import_state = project.import_state || project.build_import_state

View File

@ -418,6 +418,46 @@ RSpec.describe 'GFM autocomplete', :js do
end
end
context 'when other notes are destroyed' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
# This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
it 'keeps autocomplete key listeners' do
visit project_issue_path(project, issue)
note = find('#note-body')
start_comment_with_emoji(note)
start_and_cancel_discussion
note.fill_in(with: '')
start_comment_with_emoji(note)
note.native.send_keys(:enter)
expect(note.value).to eql('Hello :100: ')
end
def start_comment_with_emoji(note)
note.native.send_keys('Hello :10')
wait_for_requests
find('.atwho-view li', text: '100')
end
def start_and_cancel_discussion
click_button('Reply...')
fill_in('note_note', with: 'Whoops!')
page.accept_alert 'Are you sure you want to cancel creating this comment?' do
click_button('Cancel')
end
wait_for_requests
end
end
shared_examples 'autocomplete suggestions' do
it 'suggests objects correctly' do
page.within '.timeline-content-form' do
@ -550,6 +590,15 @@ RSpec.describe 'GFM autocomplete', :js do
expect(find('.tribute-container ul', visible: true)).to have_text('alert milestone')
end
it 'does not open autocomplete menu when trigger character is prefixed with text' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('testing')
find('#note-body').native.send_keys('@')
end
expect(page).not_to have_selector('.tribute-container', visible: true)
end
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
@ -618,21 +667,6 @@ RSpec.describe 'GFM autocomplete', :js do
expect(page).to have_selector('.tribute-container', visible: true)
end
it "does not show dropdown when preceded with a special character" do
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys("@")
end
expect(page).to have_selector('.tribute-container', visible: true)
page.within '.timeline-content-form' do
note.native.send_keys("@")
end
expect(page).not_to have_selector('.tribute-container')
end
it "does not throw an error if no labels exist" do
note = find('#note-body')
page.within '.timeline-content-form' do
@ -653,14 +687,6 @@ RSpec.describe 'GFM autocomplete', :js do
expect_to_wrap(false, user_item, note, user.username)
end
it 'doesn\'t open autocomplete after non-word character' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys("@#{user.username[0..2]}!")
end
expect(page).not_to have_selector('.tribute-container')
end
it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body')
page.within '.timeline-content-form' do
@ -848,46 +874,6 @@ RSpec.describe 'GFM autocomplete', :js do
it_behaves_like 'autocomplete suggestions'
end
context 'when other notes are destroyed' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
# This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
it 'keeps autocomplete key listeners' do
visit project_issue_path(project, issue)
note = find('#note-body')
start_comment_with_emoji(note)
start_and_cancel_discussion
note.fill_in(with: '')
start_comment_with_emoji(note)
note.native.send_keys(:enter)
expect(note.value).to eql('Hello :100: ')
end
def start_comment_with_emoji(note)
note.native.send_keys('Hello :10')
wait_for_requests
find('.atwho-view li', text: '100')
end
def start_and_cancel_discussion
click_button('Reply...')
fill_in('note_note', with: 'Whoops!')
page.accept_alert 'Are you sure you want to cancel creating this comment?' do
click_button('Cancel')
end
wait_for_requests
end
end
end
private

View File

@ -7,6 +7,7 @@ import {
clientTypenames,
createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
import { STATUSES } from '~/import_entities/constants';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
@ -17,6 +18,12 @@ import importGroupMutation from '~/import_entities/import_groups/graphql/mutatio
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
StatusPoller: jest.fn().mockImplementation(function mock() {
this.startPolling = jest.fn();
}),
}));
const FAKE_ENDPOINTS = {
status: '/fake_status_url',
availableNamespaces: '/fake_available_namespaces',
@ -173,6 +180,42 @@ describe('Bulk import resolvers', () => {
expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING);
});
it('sets group status to STARTED when request completes', async () => {
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
await client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
});
expect(results[0].status).toBe(STATUSES.STARTED);
});
it('starts polling when request completes', async () => {
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
await client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
});
const [statusPoller] = StatusPoller.mock.instances;
expect(statusPoller.startPolling).toHaveBeenCalled();
});
it('resets status to NONE if request fails', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.INTERNAL_SERVER_ERROR);
client
.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
})
.catch(() => {});
await waitForPromises();
expect(results[0].status).toBe(STATUSES.NONE);
});
});
});
});

View File

@ -0,0 +1,213 @@
import { createMockClient } from 'mock-apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { STATUSES } from '~/import_entities/constants';
import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
import { generateFakeEntry } from '../fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manager', () => ({
SourceGroupsManager: jest.fn().mockImplementation(function mock() {
this.setImportStatus = jest.fn();
}),
}));
const TEST_POLL_INTERVAL = 1000;
describe('Bulk import status poller', () => {
let poller;
let clientMock;
const listQueryCacheCalls = () =>
clientMock.readQuery.mock.calls.filter(call => call[0].query === bulkImportSourceGroupsQuery);
beforeEach(() => {
clientMock = createMockClient({
cache: new InMemoryCache({
fragmentMatcher: { match: () => true },
}),
});
jest.spyOn(clientMock, 'readQuery');
poller = new StatusPoller({
client: clientMock,
interval: TEST_POLL_INTERVAL,
});
});
describe('general behavior', () => {
beforeEach(() => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: { bulkImportSourceGroups: [] },
});
});
it('does not perform polling when constructed', () => {
jest.runOnlyPendingTimers();
expect(listQueryCacheCalls()).toHaveLength(0);
});
it('immediately start polling when requested', async () => {
await poller.startPolling();
expect(listQueryCacheCalls()).toHaveLength(1);
});
it('constantly polls when started', async () => {
poller.startPolling();
expect(listQueryCacheCalls()).toHaveLength(1);
jest.advanceTimersByTime(TEST_POLL_INTERVAL);
expect(listQueryCacheCalls()).toHaveLength(2);
jest.advanceTimersByTime(TEST_POLL_INTERVAL);
expect(listQueryCacheCalls()).toHaveLength(3);
});
it('does not start polling when requested multiple times', async () => {
poller.startPolling();
expect(listQueryCacheCalls()).toHaveLength(1);
poller.startPolling();
expect(listQueryCacheCalls()).toHaveLength(1);
});
it('stops polling when requested', async () => {
poller.startPolling();
expect(listQueryCacheCalls()).toHaveLength(1);
poller.stopPolling();
jest.runOnlyPendingTimers();
expect(listQueryCacheCalls()).toHaveLength(1);
});
it('does not query server when list is empty', async () => {
jest.spyOn(clientMock, 'query');
poller.startPolling();
expect(clientMock.query).not.toHaveBeenCalled();
});
});
it('does not query server when no groups have STARTED status', async () => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STATUSES.NONE, STATUSES.FINISHED].map((status, idx) =>
generateFakeEntry({ status, id: idx }),
),
},
});
jest.spyOn(clientMock, 'query');
poller.startPolling();
expect(clientMock.query).not.toHaveBeenCalled();
});
describe('when there are groups which have STARTED status', () => {
const TARGET_NAMESPACE = 'root';
const STARTED_GROUP_1 = {
status: STATUSES.STARTED,
id: 'started1',
import_target: {
target_namespace: TARGET_NAMESPACE,
new_name: 'group1',
},
};
const STARTED_GROUP_2 = {
status: STATUSES.STARTED,
id: 'started2',
import_target: {
target_namespace: TARGET_NAMESPACE,
new_name: 'group2',
},
};
const NOT_STARTED_GROUP = {
status: STATUSES.NONE,
id: 'not_started',
import_target: {
target_namespace: TARGET_NAMESPACE,
new_name: 'group3',
},
};
it('query server only for groups with STATUSES.STARTED', async () => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STARTED_GROUP_1, NOT_STARTED_GROUP, STARTED_GROUP_2].map(group =>
generateFakeEntry(group),
),
},
});
clientMock.query = jest.fn().mockResolvedValue({ data: {} });
poller.startPolling();
expect(clientMock.query).toHaveBeenCalledTimes(1);
await waitForPromises();
const [[doc]] = clientMock.query.mock.calls;
const { selections } = doc.query.definitions[0].selectionSet;
expect(selections.every(field => field.name.value === 'group')).toBeTruthy();
expect(selections).toHaveLength(2);
expect(selections.map(sel => sel.arguments[0].value.value)).toStrictEqual([
`${TARGET_NAMESPACE}/${STARTED_GROUP_1.import_target.new_name}`,
`${TARGET_NAMESPACE}/${STARTED_GROUP_2.import_target.new_name}`,
]);
});
it('updates statuses only for groups in response', async () => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map(group =>
generateFakeEntry(group),
),
},
});
clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } });
poller.startPolling();
await waitForPromises();
const [managerInstance] = SourceGroupsManager.mock.instances;
expect(managerInstance.setImportStatus).toHaveBeenCalledTimes(1);
expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
expect.objectContaining({ id: STARTED_GROUP_1.id }),
STATUSES.FINISHED,
);
});
describe('when error occurs', () => {
beforeEach(() => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map(group =>
generateFakeEntry(group),
),
},
});
clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error'));
poller.startPolling();
return waitForPromises();
});
it('reports an error', () => {
expect(createFlash).toHaveBeenCalled();
});
it('continues polling', async () => {
jest.advanceTimersByTime(TEST_POLL_INTERVAL);
expect(listQueryCacheCalls()).toHaveLength(2);
});
});
});
});

View File

@ -1,5 +1,5 @@
import Tribute from '@gitlab/tributejs';
import { shallowMount } from '@vue/test-utils';
import Tribute from 'tributejs';
import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
describe('GfmAutocomplete', () => {

View File

@ -4,14 +4,14 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::GroupMemberBuilder do
let_it_be(:group) { create(:group) }
let_it_be(:group_member) { create(:group_member, :developer, group: group) }
let_it_be(:group_member) { create(:group_member, :developer, group: group, expires_at: 1.day.from_now) }
describe '#build' do
let(:data) { described_class.new(group_member).build(event) }
let(:event_name) { data[:event_name] }
let(:attributes) do
[
:event_name, :created_at, :updated_at, :group_name, :group_path,
:event_name, :created_at, :updated_at, :expires_at, :group_name, :group_path,
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
]
end
@ -31,6 +31,7 @@ RSpec.describe Gitlab::HookData::GroupMemberBuilder do
expect(data[:group_access]).to eq('Developer')
expect(data[:created_at]).to eq(group_member.created_at&.xmlschema)
expect(data[:updated_at]).to eq(group_member.updated_at&.xmlschema)
expect(data[:expires_at]).to eq(group_member.expires_at&.xmlschema)
end
end

View File

@ -3098,6 +3098,14 @@ RSpec.describe User do
end
end
describe '#can_remove_self?' do
let(:user) { create(:user) }
it 'returns true' do
expect(user.can_remove_self?).to eq true
end
end
describe "#recent_push" do
let(:user) { build(:user) }
let(:project) { build(:project) }

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Jira::Requests::Projects::ListService do
include AfterNextHelpers
let(:jira_service) { create(:jira_service) }
let(:params) { {} }
@ -33,15 +35,17 @@ RSpec.describe Jira::Requests::Projects::ListService do
context 'with jira_service' do
context 'when validations and params are ok' do
let(:client) { double(options: { site: 'https://jira.example.com' }) }
let(:response_headers) { { 'content-type' => 'application/json' } }
let(:response_body) { [].to_json }
let(:expected_url_pattern) { /.*jira.example.com\/rest\/api\/2\/project/ }
before do
expect(service).to receive(:client).at_least(:once).and_return(client)
stub_request(:get, expected_url_pattern).to_return(status: 200, body: response_body, headers: response_headers)
end
context 'when the request to Jira returns an error' do
before do
expect(client).to receive(:get).and_raise(Timeout::Error)
expect_next(JIRA::Client).to receive(:get).and_raise(Timeout::Error)
end
it 'returns an error response' do
@ -54,10 +58,17 @@ RSpec.describe Jira::Requests::Projects::ListService do
end
end
context 'when the request does not return any values' do
before do
expect(client).to receive(:get).and_return([])
context 'when jira runs on a subpath' do
let(:jira_service) { create(:jira_service, url: 'http://jira.example.com/jira') }
let(:expected_url_pattern) { /.*jira.example.com\/jira\/rest\/api\/2\/project/ }
it 'takes the subpath into account' do
expect(subject.success?).to be_truthy
end
end
context 'when the request does not return any values' do
let(:response_body) { [].to_json }
it 'returns a paylod with no projects returned' do
payload = subject.payload
@ -69,9 +80,7 @@ RSpec.describe Jira::Requests::Projects::ListService do
end
context 'when the request returns values' do
before do
expect(client).to receive(:get).and_return([{ 'key' => 'pr1', 'name' => 'First Project' }, { 'key' => 'pr2', 'name' => 'Second Project' }])
end
let(:response_body) { [{ 'key' => 'pr1', 'name' => 'First Project' }, { 'key' => 'pr2', 'name' => 'Second Project' }].to_json }
it 'returns a paylod with Jira projects' do
payload = subject.payload

View File

@ -246,17 +246,25 @@ RSpec.describe 'gitlab:db namespace rake task' do
context 'with index name given' do
let(:index) { double('index') }
before do
allow(Gitlab::Database::Reindexing).to receive(:candidate_indexes).and_return(indexes)
end
it 'calls the index rebuilder with the proper arguments' do
expect(Gitlab::Database::PostgresIndex).to receive(:by_identifier).with('public.foo_idx').and_return(index)
allow(indexes).to receive(:where).with(identifier: 'public.foo_idx').and_return([index])
expect(Gitlab::Database::Reindexing).to receive(:perform).with([index])
run_rake_task('gitlab:db:reindex', '[public.foo_idx]')
end
it 'raises an error if the index does not exist' do
expect(Gitlab::Database::PostgresIndex).to receive(:by_identifier).with('public.absent_index').and_raise(ActiveRecord::RecordNotFound)
allow(indexes).to receive(:where).with(identifier: 'public.absent_index').and_return([])
expect { run_rake_task('gitlab:db:reindex', '[public.absent_index]') }.to raise_error(ActiveRecord::RecordNotFound)
expect { run_rake_task('gitlab:db:reindex', '[public.absent_index]') }.to raise_error(/Index not found/)
end
it 'raises an error if the index is not fully qualified with a schema' do
expect { run_rake_task('gitlab:db:reindex', '[foo_idx]') }.to raise_error(/Index name is not fully qualified/)
end
end
end

View File

@ -866,6 +866,11 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.177.0.tgz#e481ed327a11d3834c8b1668d7485b9eefef97f5"
integrity sha512-L7DggusgkbubNFCRIYtCuYiLx+t5Hp8y/XIxJ3RM5mqAfxkTR1KxALNLDP9CT7xWieHDhNvgcXAdamGoi0ofDQ==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@24.8.0":
version "24.8.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-24.8.0.tgz#dc7daf941ba691e702d607e0a31377a374fdb136"
@ -11749,11 +11754,6 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
tributejs@5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"