Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0ff373dc41
commit
b07852468f
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
fragment PipelineStagesData on CiConfigStage {
|
||||
name
|
||||
groups {
|
||||
name
|
||||
jobs {
|
||||
name
|
||||
needs {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
fragment PipelineStagesConnection on CiConfigStageConnection {
|
||||
nodes {
|
||||
name
|
||||
groups {
|
||||
nodes {
|
||||
name
|
||||
jobs {
|
||||
nodes {
|
||||
name
|
||||
needs {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add expires_at param to GroupMemberBuilder data
|
||||
merge_request: 49981
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow job to download artifacts in parent-child pipeline hierarchy
|
||||
merge_request: 49837
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Introduce frontend for group migration MVC
|
||||
merge_request: 49709
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue