Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ed50918678
commit
e36443c1d6
35 changed files with 495 additions and 71 deletions
2
Gemfile
2
Gemfile
|
@ -142,7 +142,7 @@ gem 'carrierwave', '~> 1.3'
|
|||
gem 'mini_magick', '~> 4.10.1'
|
||||
|
||||
# for backups
|
||||
gem 'fog-aws', '~> 3.14'
|
||||
gem 'fog-aws', '~> 3.15'
|
||||
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
|
||||
# Also see config/initializers/fog_core_patch.rb.
|
||||
gem 'fog-core', '= 2.1.0'
|
||||
|
|
|
@ -180,7 +180,7 @@
|
|||
{"name":"flipper-active_support_cache_store","version":"0.25.0","platform":"ruby","checksum":"7282bf994b08d1a076b65c6f3b51e3dc04fcb00fa6e7b20089e60db25c7b531b"},
|
||||
{"name":"flowdock","version":"0.7.1","platform":"ruby","checksum":"cfa95b2ac96e5f883f6e419d7a891f76cfcc17a28c416b6b714bbdffc8dbd912"},
|
||||
{"name":"fog-aliyun","version":"0.3.3","platform":"ruby","checksum":"d0aa317f7c1473a1d684fff51699f216bb9cb78b9ee9ce55a81c9bcc93fb85ee"},
|
||||
{"name":"fog-aws","version":"3.14.0","platform":"ruby","checksum":"07442dff8ee2a314413f812d6f6052e7d4a444540df84c193c135c1994114bbf"},
|
||||
{"name":"fog-aws","version":"3.15.0","platform":"ruby","checksum":"09752931ea0c6165b018e1a89253248d86b246645086ccf19bc44fabe3381e8c"},
|
||||
{"name":"fog-core","version":"2.1.0","platform":"ruby","checksum":"53e5d793554d7080d015ef13cd44b54027e421d924d9dba4ce3d83f95f37eda9"},
|
||||
{"name":"fog-google","version":"1.15.0","platform":"ruby","checksum":"2f840780fbf2384718e961b05ef2fc522b4213bbda6f25b28c1bbd875ff0b306"},
|
||||
{"name":"fog-json","version":"1.2.0","platform":"ruby","checksum":"dd4f5ab362dbc72b687240bba9d2dd841d5dfe888a285797533f85c03ea548fe"},
|
||||
|
|
|
@ -492,7 +492,7 @@ GEM
|
|||
fog-json
|
||||
ipaddress (~> 0.8)
|
||||
xml-simple (~> 1.1)
|
||||
fog-aws (3.14.0)
|
||||
fog-aws (3.15.0)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (~> 1.1)
|
||||
fog-xml (~> 0.1)
|
||||
|
@ -1614,7 +1614,7 @@ DEPENDENCIES
|
|||
flipper-active_support_cache_store (~> 0.25.0)
|
||||
flowdock (~> 0.7)
|
||||
fog-aliyun (~> 0.3)
|
||||
fog-aws (~> 3.14)
|
||||
fog-aws (~> 3.15)
|
||||
fog-core (= 2.1.0)
|
||||
fog-google (~> 1.15)
|
||||
fog-local (~> 0.6)
|
||||
|
|
|
@ -94,9 +94,9 @@ export const getUserDataByProp = (state) => (prop) => state.userData && state.us
|
|||
export const descriptionVersions = (state) => state.descriptionVersions;
|
||||
|
||||
export const canUserAddIncidentTimelineEvents = (state) => {
|
||||
return (
|
||||
state.userData.can_add_timeline_events &&
|
||||
state.noteableData.type === constants.NOTEABLE_TYPE_MAPPING.Incident
|
||||
return Boolean(
|
||||
state.userData?.can_add_timeline_events &&
|
||||
state.noteableData.type === constants.NOTEABLE_TYPE_MAPPING.Incident,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -99,7 +99,6 @@ export const LEGACY_FILE_TYPES = [
|
|||
'podspec',
|
||||
'podspec_json',
|
||||
'cartfile',
|
||||
'godeps_json',
|
||||
'requirements_txt',
|
||||
'cargo_toml',
|
||||
'go_mod',
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import packageJsonLinker from './utils/package_json_linker';
|
||||
import gemspecLinker from './utils/gemspec_linker';
|
||||
import godepsJsonLinker from './utils/godeps_json_linker';
|
||||
|
||||
const DEPENDENCY_LINKERS = {
|
||||
package_json: packageJsonLinker,
|
||||
gemspec: gemspecLinker,
|
||||
godeps_json: godepsJsonLinker,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
|
||||
|
||||
const PROTOCOL = 'https://';
|
||||
const GODOCS_DOMAIN = 'godoc.org/';
|
||||
const REPO_PATH = '/tree/master/';
|
||||
const GODOCS_REGEX = /golang.org/;
|
||||
const GITLAB_REPO_PATH = `/_${REPO_PATH}`;
|
||||
const REPO_REGEX = `[^/'"]+/[^/'"]+`;
|
||||
const NESTED_REPO_REGEX = '([^/]+/)+[^/]+?';
|
||||
const GITHUB_REPO_REGEX = new RegExp(`(github.com/${REPO_REGEX})/(.+)`);
|
||||
const GITLAB_REPO_REGEX = new RegExp(`(gitlab.com/${REPO_REGEX})/(.+)`);
|
||||
const GITLAB_NESTED_REPO_REGEX = new RegExp(`(gitlab.com/${NESTED_REPO_REGEX}).git/(.+)`);
|
||||
const attrOpenTag = generateHLJSOpenTag('attr');
|
||||
const stringOpenTag = generateHLJSOpenTag('string');
|
||||
const closeTag = '"</span>';
|
||||
const importPathString =
|
||||
'ImportPath"</span><span class="hljs-punctuation">:</span><span class=""> </span>';
|
||||
|
||||
const DEPENDENCY_REGEX = new RegExp(
|
||||
/*
|
||||
* Detects dependencies inside of content that is highlighted by Highlight.js
|
||||
* Example: <span class="hljs-attr">"ImportPath"</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">"github.com/ayufan/golang-kardianos-service"</span>
|
||||
* Group 1: github.com/ayufan/golang-kardianos-service
|
||||
*/
|
||||
`${importPathString}${stringOpenTag}(.*)${closeTag}`,
|
||||
'gm',
|
||||
);
|
||||
|
||||
const replaceRepoPath = (dependency, regex, repoPath) =>
|
||||
dependency.replace(regex, (_, repo, path) => `${PROTOCOL}${repo}${repoPath}${path}`);
|
||||
|
||||
const regexConfigs = [
|
||||
{
|
||||
matcher: GITHUB_REPO_REGEX,
|
||||
resolver: (dep) => replaceRepoPath(dep, GITHUB_REPO_REGEX, REPO_PATH),
|
||||
},
|
||||
{
|
||||
matcher: GITLAB_REPO_REGEX,
|
||||
resolver: (dep) => replaceRepoPath(dep, GITLAB_REPO_REGEX, GITLAB_REPO_PATH),
|
||||
},
|
||||
{
|
||||
matcher: GITLAB_NESTED_REPO_REGEX,
|
||||
resolver: (dep) => replaceRepoPath(dep, GITLAB_NESTED_REPO_REGEX, GITLAB_REPO_PATH),
|
||||
},
|
||||
{
|
||||
matcher: GODOCS_REGEX,
|
||||
resolver: (dep) => `${PROTOCOL}${GODOCS_DOMAIN}${dep}`,
|
||||
},
|
||||
];
|
||||
|
||||
const getLinkHref = (dependency) => {
|
||||
const regexConfig = regexConfigs.find((config) => dependency.match(config.matcher));
|
||||
return regexConfig ? regexConfig.resolver(dependency) : `${PROTOCOL}${dependency}`;
|
||||
};
|
||||
|
||||
const handleReplace = (dependency) => {
|
||||
const linkHref = getLinkHref(dependency);
|
||||
const link = createLink(linkHref, dependency);
|
||||
return `${importPathString}${attrOpenTag}${link}${closeTag}`;
|
||||
};
|
||||
|
||||
export default (result) => {
|
||||
return result.value.replace(DEPENDENCY_REGEX, (_, dependency) => handleReplace(dependency));
|
||||
};
|
|
@ -1,6 +1,4 @@
|
|||
.user-contrib-cell {
|
||||
stroke: $t-gray-a-08;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
stroke: $black;
|
||||
|
|
|
@ -346,6 +346,20 @@ $theme-light-red-500: #c24b38;
|
|||
$theme-light-red-600: #b03927;
|
||||
$theme-light-red-700: #a62e21;
|
||||
|
||||
// Data visualization color palette
|
||||
|
||||
$data-viz-blue-50: #e9ebff;
|
||||
$data-viz-blue-100: #d4dcfa;
|
||||
$data-viz-blue-200: #b7c6ff;
|
||||
$data-viz-blue-300: #97acff;
|
||||
$data-viz-blue-400: #748eff;
|
||||
$data-viz-blue-500: #5772ff;
|
||||
$data-viz-blue-600: #445cf2;
|
||||
$data-viz-blue-700: #3547de;
|
||||
$data-viz-blue-800: #232fcf;
|
||||
$data-viz-blue-900: #1e23a8;
|
||||
$data-viz-blue-950: #11118a;
|
||||
|
||||
$border-white-light: darken($white, $darken-border-factor) !default;
|
||||
$border-white-normal: darken($white-normal, $darken-border-factor) !default;
|
||||
|
||||
|
@ -710,11 +724,11 @@ $job-arrow-margin: 55px;
|
|||
*/
|
||||
// See https://gitlab.com/gitlab-org/gitlab/-/issues/332150 to align with Pajamas Design System
|
||||
$calendar-activity-colors: (
|
||||
#f5f5f5,
|
||||
#d4dcfa,
|
||||
#748eff,
|
||||
#3547de,
|
||||
#11118a,
|
||||
$gray-50,
|
||||
$data-viz-blue-100,
|
||||
$data-viz-blue-400,
|
||||
$data-viz-blue-700,
|
||||
$data-viz-blue-950,
|
||||
) !default;
|
||||
|
||||
/*
|
||||
|
|
|
@ -33,7 +33,9 @@ module Ci
|
|||
def routing_table_enabled?
|
||||
return false if routing_class?
|
||||
|
||||
::Feature.enabled?(routing_table_name_flag)
|
||||
Gitlab::SafeRequestStore.fetch(routing_table_name_flag) do
|
||||
::Feature.enabled?(routing_table_name_flag)
|
||||
end
|
||||
end
|
||||
|
||||
# We're delegating them to the `Partitioned` model.
|
||||
|
|
|
@ -8,5 +8,7 @@ module Users
|
|||
belongs_to :initiator_user, class_name: 'User'
|
||||
|
||||
validates :user_id, presence: true
|
||||
|
||||
scope :consume_order, -> { order(:consume_after, :id) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,25 +2,38 @@
|
|||
|
||||
module Users
|
||||
class MigrateRecordsToGhostUserInBatchesService
|
||||
LIMIT_SIZE = 1000
|
||||
|
||||
def initialize
|
||||
@execution_tracker = Gitlab::Utils::ExecutionTracker.new
|
||||
end
|
||||
|
||||
def execute
|
||||
Users::GhostUserMigration.find_each do |user_to_migrate|
|
||||
ghost_user_migrations.each do |job|
|
||||
break if execution_tracker.over_limit?
|
||||
|
||||
service = Users::MigrateRecordsToGhostUserService.new(user_to_migrate.user,
|
||||
user_to_migrate.initiator_user,
|
||||
service = Users::MigrateRecordsToGhostUserService.new(job.user,
|
||||
job.initiator_user,
|
||||
execution_tracker)
|
||||
service.execute(hard_delete: user_to_migrate.hard_delete)
|
||||
service.execute(hard_delete: job.hard_delete)
|
||||
rescue Gitlab::Utils::ExecutionTracker::ExecutionTimeOutError
|
||||
# no-op
|
||||
rescue StandardError => e
|
||||
::Gitlab::ErrorTracking.track_exception(e)
|
||||
reschedule(job)
|
||||
end
|
||||
rescue Gitlab::Utils::ExecutionTracker::ExecutionTimeOutError
|
||||
# no-op
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :execution_tracker
|
||||
|
||||
def ghost_user_migrations
|
||||
Users::GhostUserMigration.consume_order.limit(LIMIT_SIZE)
|
||||
end
|
||||
|
||||
def reschedule(job)
|
||||
job.update(consume_after: 30.minutes.from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddConsumeAfterToGhostUserMigrations < Gitlab::Database::Migration[2.0]
|
||||
def change
|
||||
add_column :ghost_user_migrations, :consume_after, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddConsumeAfterIndexToGhostUserMigrations < Gitlab::Database::Migration[2.0]
|
||||
INDEX_NAME = 'index_ghost_user_migrations_on_consume_after_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :ghost_user_migrations, [:consume_after, :id], name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :ghost_user_migrations, INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20221018124029
Normal file
1
db/schema_migrations/20221018124029
Normal file
|
@ -0,0 +1 @@
|
|||
c3a38f280c8835e77953b69ba41ef5d58b76fd5f2f39e758a523c493306b0ab2
|
1
db/schema_migrations/20221018124035
Normal file
1
db/schema_migrations/20221018124035
Normal file
|
@ -0,0 +1 @@
|
|||
77aca033a7c58af4e981136b96629acf5b82a42701072928532681dd91b05280
|
|
@ -15859,7 +15859,8 @@ CREATE TABLE ghost_user_migrations (
|
|||
initiator_user_id bigint,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
hard_delete boolean DEFAULT false NOT NULL
|
||||
hard_delete boolean DEFAULT false NOT NULL,
|
||||
consume_after timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ghost_user_migrations_id_seq
|
||||
|
@ -29059,6 +29060,8 @@ CREATE INDEX index_geo_repository_updated_events_on_source ON geo_repository_upd
|
|||
|
||||
CREATE INDEX index_geo_reset_checksum_events_on_project_id ON geo_reset_checksum_events USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_ghost_user_migrations_on_consume_after_id ON ghost_user_migrations USING btree (consume_after, id);
|
||||
|
||||
CREATE UNIQUE INDEX index_ghost_user_migrations_on_user_id ON ghost_user_migrations USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_gin_ci_namespace_mirrors_on_traversal_ids ON ci_namespace_mirrors USING gin (traversal_ids);
|
||||
|
|
|
@ -192,8 +192,7 @@ For historical reasons
|
|||
[GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell) contains
|
||||
the Git hooks that allow GitLab to validate and react to Git pushes.
|
||||
Because Gitaly "owns" Git pushes, GitLab Shell must therefore be
|
||||
installed alongside Gitaly. We plan to
|
||||
[simplify this](https://gitlab.com/gitlab-org/gitaly/-/issues/1226).
|
||||
installed alongside Gitaly.
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
| ---- | ---- | -------- | ----------- |
|
||||
|
|
|
@ -619,6 +619,67 @@ Supported attributes:
|
|||
| `include_rebase_in_progress` | boolean | **{dotted-circle}** No | If `true`, response includes whether a rebase operation is in progress. |
|
||||
| `render_html` | boolean | **{dotted-circle}** No | If `true`, response includes rendered HTML for title and description. |
|
||||
|
||||
### Response
|
||||
|
||||
| Attribute | Type | Description |
|
||||
|----------------------------------|------|-------------|
|
||||
| `approvals_before_merge` | integer | **(PREMIUM)** Number of approvals required before this can be merged. |
|
||||
| `assignee` | object | First assignee of the merge request. |
|
||||
| `assignees` | array | Assignees of the merge request. |
|
||||
| `author` | object | User who created this merge request. |
|
||||
| `blocking_discussions_resolved` | boolean | Indicates if all discussions are resolved only if all are required before merge request can be merged. |
|
||||
| `changes_count` | string | Number of changes made on the merge request. |
|
||||
| `closed_at` | datetime | Timestamp of when the merge request was closed. |
|
||||
| `closed_by` | object | User who closed this merge request. |
|
||||
| `created_at` | datetime | Timestamp of when the merge request was created. |
|
||||
| `description` | string | Description of the merge request (Markdown rendered as HTML for caching). |
|
||||
| `detailed_merge_status` | string | Detailed merge status of the merge request. |
|
||||
| `diff_refs` | object | References of the base SHA, the head SHA, and the start SHA for this merge request. |
|
||||
| `discussion_locked` | boolean | Indicates if comments on the merge request are locked to members only. |
|
||||
| `downvotes` | integer | Number of downvotes for the merge request. |
|
||||
| `draft` | boolean | Indicates if the merge request is a draft. |
|
||||
| `first_contribution` | boolean | Indicates if the merge request is the first contribution of the author. |
|
||||
| `first_deployed_to_production_at` | datetime | Timestamp of when the first deployment finished. |
|
||||
| `force_remove_source_branch` | boolean | Indicates if the project settings will lead to source branch deletion after merge. |
|
||||
| `has_conflicts` | boolean | Indicates if merge request has conflicts and cannot be merged. |
|
||||
| `head_pipeline` | object | Pipeline running on the branch HEAD of the merge request. |
|
||||
| `id` | integer | ID of the merge request. |
|
||||
| `iid` | integer | Internal ID of the merge request. |
|
||||
| `labels` | array | Labels of the merge request. |
|
||||
| `latest_build_finished_at` | datetime | Timestamp of when the latest build for the merge request finished. |
|
||||
| `latest_build_started_at` | datetime | Timestamp of when the latest build for the merge request started. |
|
||||
| `merge_commit_sha` | string | SHA of the merge request commit (set once merged). |
|
||||
| `merge_error` | string | Error message due to a merge error. |
|
||||
| `merge_user` | object | User who merged this merge request or set it to merge when pipeline succeeds. |
|
||||
| `merge_status` | string | Status of the merge request. Can be `unchecked`, `checking`, `can_be_merged`, `cannot_be_merged` or `cannot_be_merged_recheck`. |
|
||||
| `merge_when_pipeline_succeeds` | boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS). |
|
||||
| `merged_at` | datetime | Timestamp of when the merge request was merged. |
|
||||
| `merged_by` | object | Deprecated: Use `merge_user` instead. User who merged this merge request or set it to merge when pipeline succeeds. |
|
||||
| `milestone` | object | Milestone of the merge request. |
|
||||
| `pipeline` | object | Pipeline running on the branch HEAD of the merge request. |
|
||||
| `project_id` | integer | ID of the merge request project. |
|
||||
| `reference` | string | Deprecated: Use `references` instead. Internal reference of the merge request. Returned in shortened format by default. |
|
||||
| `references` | object | Internal references of the merge request. Includes `short`, `relative` and `full` references. |
|
||||
| `reviewers` | array | Reviewers of the merge request. |
|
||||
| `sha` | string | Diff head SHA of the merge request. |
|
||||
| `should_remove_source_branch` | boolean | Indicates if the source branch of the merge request will be deleted after merge. |
|
||||
| `source_branch` | string | Source branch of the merge request. |
|
||||
| `source_project_id` | integer | ID of the merge request source project. |
|
||||
| `squash` | boolean | Indicates if squash on merge is enabled. |
|
||||
| `squash_commit_sha` | string | SHA of the squash commit (set once merged). |
|
||||
| `state` | string | State of the merge request. Can be `opened`, `closed`, `merged` or `locked`. |
|
||||
| `subscribed` | boolean | Indicates if the currently logged in user is subscribed to this merge request. |
|
||||
| `target_branch` | string | Target branch of the merge request. |
|
||||
| `target_project_id` | integer | ID of the merge request target project. |
|
||||
| `task_completion_status` | object | Completion status of tasks. |
|
||||
| `title` | string | Title of the merge request. |
|
||||
| `updated_at` | datetime | Timestamp of when the merge request was updated. |
|
||||
| `upvotes` | integer | Number of upvotes for the merge request. |
|
||||
| `user` | object | Permissions of the user requested for the merge request. |
|
||||
| `user_notes_count` | integer | User notes count of the merge request. |
|
||||
| `web_url` | string | Web URL of the merge request. |
|
||||
| `work_in_progress` | boolean | Deprecated: Use `draft` instead. Indicates if the merge request is a draft. |
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 155016530,
|
||||
|
@ -787,7 +848,7 @@ the `approvals_before_merge` parameter:
|
|||
|
||||
### Merge status
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6.
|
||||
> The `detailed_merge_status` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6.
|
||||
|
||||
- The `merge_status` field may hold one of the following values:
|
||||
- `unchecked`: This merge request has not yet been checked.
|
||||
|
|
|
@ -25,11 +25,11 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
|
|||
- You can generate and use a [Bitbucket App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) for the password field.
|
||||
|
||||
GitLab imports the repository and enables [Pull Mirroring](../../user/project/repository/mirror/pull.md).
|
||||
You can check that mirroring is working in the project by going to **Settings > Repository > Mirroring repositories**.
|
||||
You can check that mirroring is working in the project in **Settings > Repository > Mirroring repositories**.
|
||||
|
||||
1. In GitLab, create a
|
||||
[Personal Access Token](../../user/profile/personal_access_tokens.md)
|
||||
with `api` scope. This is used to authenticate requests from the web
|
||||
with `api` scope. The token is used to authenticate requests from the web
|
||||
hook that is created in Bitbucket to notify GitLab of new commits.
|
||||
|
||||
1. In Bitbucket, from **Settings > Webhooks**, create a new web hook to notify
|
||||
|
@ -58,18 +58,14 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
|
|||
1. In GitLab, from **Settings > CI/CD > Variables**, add variables to allow
|
||||
communication with Bitbucket via the Bitbucket API:
|
||||
|
||||
`BITBUCKET_ACCESS_TOKEN`: the Bitbucket app password created above.
|
||||
- `BITBUCKET_ACCESS_TOKEN`: The Bitbucket app password created above. This variable should be [masked](../variables/index.md#mask-a-cicd-variable).
|
||||
- `BITBUCKET_USERNAME`: The username of the Bitbucket account.
|
||||
- `BITBUCKET_NAMESPACE`: Set this variable if your GitLab and Bitbucket namespaces differ.
|
||||
- `BITBUCKET_REPOSITORY`: Set this variable if your GitLab and Bitbucket project names differ.
|
||||
|
||||
`BITBUCKET_USERNAME`: the username of the Bitbucket account.
|
||||
|
||||
`BITBUCKET_NAMESPACE`: set this if your GitLab and Bitbucket namespaces differ.
|
||||
|
||||
`BITBUCKET_REPOSITORY`: set this if your GitLab and Bitbucket project names differ.
|
||||
|
||||
1. In Bitbucket, add a script to push the pipeline status to Bitbucket.
|
||||
|
||||
NOTE:
|
||||
The changes must be made in Bitbucket as any changes in the GitLab repository are overwritten by Bitbucket when GitLab next mirrors the repository.
|
||||
1. In Bitbucket, add a script that pushes the pipeline status to Bitbucket. The script
|
||||
is created in Bitbucket, but the mirroring process copies it to the GitLab mirror. The GitLab
|
||||
CI/CD pipeline runs the script, and pushes the status back to Bitbucket.
|
||||
|
||||
Create a file `build_status` and insert the script below and run
|
||||
`chmod +x build_status` in your terminal to make the script executable.
|
||||
|
@ -125,7 +121,8 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
|
|||
```
|
||||
|
||||
1. In Bitbucket, create a `.gitlab-ci.yml` file to use the script to push
|
||||
pipeline success and failures to Bitbucket.
|
||||
pipeline success and failures to Bitbucket. Similar to the script added above,
|
||||
this file is copied to the GitLab repo as part of the mirroring process.
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
|
|
|
@ -463,12 +463,17 @@ use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make sure to
|
|||
|
||||
The `n_` and `n__` methods should only be used to fetch pluralized translations of the same
|
||||
string, not to control the logic of showing different strings for different
|
||||
quantities. Some languages have different quantities of target plural forms.
|
||||
quantities. For similar strings, pluralize the entire sentence to provide the most context
|
||||
when translating. Some languages have different quantities of target plural forms.
|
||||
For example, Chinese (simplified) has only one target plural form in our
|
||||
translation tool. This means the translator has to choose to translate only one
|
||||
of the strings, and the translation doesn't behave as intended in the other case.
|
||||
|
||||
For example, use this:
|
||||
Below are some examples:
|
||||
|
||||
Example 1: For different strings
|
||||
|
||||
Use this:
|
||||
|
||||
```ruby
|
||||
if selected_projects.one?
|
||||
|
@ -485,6 +490,27 @@ Instead of this:
|
|||
format(n_("%{project_name}", "%d projects selected", count), project_name: 'GitLab')
|
||||
```
|
||||
|
||||
Example 2: For similar strings
|
||||
|
||||
Use this:
|
||||
|
||||
```ruby
|
||||
n__('Last day', 'Last %d days', days.length)
|
||||
```
|
||||
|
||||
Instead of this:
|
||||
|
||||
```ruby
|
||||
# incorrect usage example
|
||||
const pluralize = n__('day', 'days', days.length)
|
||||
|
||||
if (days.length === 1 ) {
|
||||
return sprintf(s__('Last %{pluralize}', pluralize)
|
||||
}
|
||||
|
||||
return sprintf(s__('Last %{dayNumber} %{pluralize}'), { dayNumber: days.length, pluralize })
|
||||
```
|
||||
|
||||
### Namespaces
|
||||
|
||||
A namespace is a way to group translations that belong together. They provide context to our
|
||||
|
|
|
@ -189,6 +189,7 @@ module API
|
|||
mount ::API::Release::Links
|
||||
mount ::API::ResourceAccessTokens
|
||||
mount ::API::SnippetRepositoryStorageMoves
|
||||
mount ::API::ProtectedBranches
|
||||
mount ::API::Statistics
|
||||
mount ::API::Suggestions
|
||||
mount ::API::Tags
|
||||
|
@ -296,7 +297,6 @@ module API
|
|||
mount ::API::ProjectStatistics
|
||||
mount ::API::ProjectTemplates
|
||||
mount ::API::Projects
|
||||
mount ::API::ProtectedBranches
|
||||
mount ::API::ProtectedTags
|
||||
mount ::API::PypiPackages
|
||||
mount ::API::Releases
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
module API
|
||||
module Entities
|
||||
class ProtectedBranch < Grape::Entity
|
||||
expose :id
|
||||
expose :name
|
||||
expose :push_access_levels, using: Entities::ProtectedRefAccess
|
||||
expose :merge_access_levels, using: Entities::ProtectedRefAccess
|
||||
expose :allow_force_push
|
||||
expose :id, documentation: { type: 'integer', example: 1 }
|
||||
expose :name, documentation: { type: 'string', example: 'main' }
|
||||
expose :push_access_levels, using: Entities::ProtectedRefAccess, documentation: { is_array: true }
|
||||
expose :merge_access_levels, using: Entities::ProtectedRefAccess, documentation: { is_array: true }
|
||||
expose :allow_force_push, documentation: { type: 'boolean' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
module API
|
||||
module Entities
|
||||
class ProtectedRefAccess < Grape::Entity
|
||||
expose :id
|
||||
expose :access_level
|
||||
expose :access_level_description do |protected_ref_access|
|
||||
protected_ref_access.humanize
|
||||
end
|
||||
expose :id, documentation: { type: 'integer', example: 1 }
|
||||
expose :access_level, documentation: { type: 'integer', example: 40 }
|
||||
expose :access_level_description,
|
||||
documentation: { type: 'string', example: 'Maintainers' } do |protected_ref_access|
|
||||
protected_ref_access.humanize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,15 +13,20 @@ module API
|
|||
helpers Helpers::ProtectedBranchesHelpers
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
requires :id, type: String, desc: 'The ID of a project', documentation: { example: 'gitlab-org/gitlab' }
|
||||
end
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc "Get a project's protected branches" do
|
||||
success Entities::ProtectedBranch
|
||||
success code: 200, model: Entities::ProtectedBranch
|
||||
is_array true
|
||||
failure [
|
||||
{ code: 404, message: '404 Project Not Found' },
|
||||
{ code: 401, message: '401 Unauthorized' }
|
||||
]
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
optional :search, type: String, desc: 'Search for a protected branch by name'
|
||||
optional :search, type: String, desc: 'Search for a protected branch by name', documentation: { example: 'mai' }
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
get ':id/protected_branches' do
|
||||
|
@ -36,10 +41,14 @@ module API
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
desc 'Get a single protected branch' do
|
||||
success Entities::ProtectedBranch
|
||||
success code: 200, model: Entities::ProtectedBranch
|
||||
failure [
|
||||
{ code: 404, message: '404 Project Not Found' },
|
||||
{ code: 401, message: '401 Unauthorized' }
|
||||
]
|
||||
end
|
||||
params do
|
||||
requires :name, type: String, desc: 'The name of the branch or wildcard'
|
||||
requires :name, type: String, desc: 'The name of the branch or wildcard', documentation: { example: 'main' }
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
get ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
|
||||
|
@ -50,10 +59,16 @@ module API
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
desc 'Protect a single branch' do
|
||||
success Entities::ProtectedBranch
|
||||
success code: 201, model: Entities::ProtectedBranch
|
||||
failure [
|
||||
{ code: 422, message: 'name is missing' },
|
||||
{ code: 409, message: "Protected branch 'main' already exists" },
|
||||
{ code: 404, message: '404 Project Not Found' },
|
||||
{ code: 401, message: '401 Unauthorized' }
|
||||
]
|
||||
end
|
||||
params do
|
||||
requires :name, type: String, desc: 'The name of the protected branch'
|
||||
requires :name, type: String, desc: 'The name of the protected branch', documentation: { example: 'main' }
|
||||
optional :push_access_level, type: Integer,
|
||||
values: ProtectedBranch::PushAccessLevel.allowed_access_levels,
|
||||
desc: 'Access levels allowed to push (defaults: `40`, maintainer access level)'
|
||||
|
@ -87,10 +102,15 @@ module API
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
desc 'Update a protected branch' do
|
||||
success ::API::Entities::ProtectedBranch
|
||||
success code: 200, model: Entities::ProtectedBranch
|
||||
failure [
|
||||
{ code: 422, message: 'Push access levels access level has already been taken' },
|
||||
{ code: 404, message: '404 Project Not Found' },
|
||||
{ code: 401, message: '401 Unauthorized' }
|
||||
]
|
||||
end
|
||||
params do
|
||||
requires :name, type: String, desc: 'The name of the branch'
|
||||
requires :name, type: String, desc: 'The name of the branch', documentation: { example: 'main' }
|
||||
optional :allow_force_push, type: Boolean,
|
||||
desc: 'Allow force push for all users with push access.'
|
||||
|
||||
|
@ -114,7 +134,14 @@ module API
|
|||
|
||||
desc 'Unprotect a single branch'
|
||||
params do
|
||||
requires :name, type: String, desc: 'The name of the protected branch'
|
||||
requires :name, type: String, desc: 'The name of the protected branch', documentation: { example: 'main' }
|
||||
end
|
||||
desc 'Unprotect a single branch' do
|
||||
success code: 204
|
||||
failure [
|
||||
{ code: 404, message: '404 Project Not Found' },
|
||||
{ code: 401, message: '401 Unauthorized' }
|
||||
]
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS, urgency: :low do
|
||||
|
|
|
@ -5,5 +5,6 @@ FactoryBot.define do
|
|||
association :user
|
||||
initiator_user { association(:user) }
|
||||
hard_delete { false }
|
||||
consume_after { Time.current }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ import { TEST_HOST } from 'spec/test_constants';
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||
import noteActions from '~/notes/components/note_actions.vue';
|
||||
import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
|
||||
import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
|
||||
import { userDataMock } from '../mock_data';
|
||||
|
@ -18,6 +20,23 @@ describe('noteActions', () => {
|
|||
|
||||
const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
|
||||
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
|
||||
const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
|
||||
|
||||
const setupStoreForIncidentTimelineEvents = ({
|
||||
userCanAdd,
|
||||
noteableType,
|
||||
isPromotionInProgress = true,
|
||||
}) => {
|
||||
store.dispatch('setUserData', {
|
||||
...userDataMock,
|
||||
can_add_timeline_events: userCanAdd,
|
||||
});
|
||||
store.state.noteableData = {
|
||||
...store.state.noteableData,
|
||||
type: noteableType,
|
||||
};
|
||||
store.state.isPromoteCommentToTimelineEventInProgress = isPromotionInProgress;
|
||||
};
|
||||
|
||||
const mountNoteActions = (propsData, computed) => {
|
||||
return mount(noteActions, {
|
||||
|
@ -238,7 +257,8 @@ describe('noteActions', () => {
|
|||
|
||||
describe('user is not logged in', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('setUserData', {});
|
||||
// userData can be null https://gitlab.com/gitlab-org/gitlab/-/issues/379375
|
||||
store.dispatch('setUserData', null);
|
||||
wrapper = mountNoteActions({
|
||||
...props,
|
||||
canDelete: false,
|
||||
|
@ -301,4 +321,56 @@ describe('noteActions', () => {
|
|||
expect(resolveButton.attributes('title')).toBe('Thread stays unresolved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline event button', () => {
|
||||
// why: We are working with an integrated store, so let's imply the getter is used
|
||||
describe.each`
|
||||
desc | userCanAdd | noteableType | exists
|
||||
${'default'} | ${true} | ${NOTEABLE_TYPE_MAPPING.Incident} | ${true}
|
||||
${'when cannot add incident timeline event'} | ${false} | ${NOTEABLE_TYPE_MAPPING.Incident} | ${false}
|
||||
${'when is not incident'} | ${true} | ${NOTEABLE_TYPE_MAPPING.MergeRequest} | ${false}
|
||||
`('$desc', ({ userCanAdd, noteableType, exists }) => {
|
||||
beforeEach(() => {
|
||||
setupStoreForIncidentTimelineEvents({
|
||||
userCanAdd,
|
||||
noteableType,
|
||||
});
|
||||
|
||||
wrapper = mountNoteActions({ ...props });
|
||||
});
|
||||
|
||||
it(`handles rendering of timeline button (exists=${exists})`, () => {
|
||||
expect(findTimelineButton().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
setupStoreForIncidentTimelineEvents({
|
||||
userCanAdd: true,
|
||||
noteableType: NOTEABLE_TYPE_MAPPING.Incident,
|
||||
});
|
||||
|
||||
wrapper = mountNoteActions({ ...props });
|
||||
});
|
||||
|
||||
it('should render timeline-event-button', () => {
|
||||
expect(findTimelineButton().props()).toEqual({
|
||||
noteId: props.noteId,
|
||||
isPromotionInProgress: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('when timeline-event-button emits click-promote-comment-to-event, dispatches action', () => {
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
|
||||
findTimelineButton().vm.$emit('click-promote-comment-to-event');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.dispatch).toHaveBeenCalledWith('promoteCommentToTimelineEvent');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
|
||||
import { DESC, ASC } from '~/notes/constants';
|
||||
import { DESC, ASC, NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
|
||||
import * as getters from '~/notes/stores/getters';
|
||||
import {
|
||||
notesDataMock,
|
||||
|
@ -536,4 +536,24 @@ describe('Getters Notes Store', () => {
|
|||
expect(getters.sortDirection(state)).toBe(DESC);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canUserAddIncidentTimelineEvents', () => {
|
||||
it.each`
|
||||
userData | noteableData | expected
|
||||
${{ can_add_timeline_events: true }} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${true}
|
||||
${{ can_add_timeline_events: true }} | ${{ type: NOTEABLE_TYPE_MAPPING.Issue }} | ${false}
|
||||
${null} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${false}
|
||||
${{ can_add_timeline_events: false }} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${false}
|
||||
`(
|
||||
'with userData=$userData and noteableData=$noteableData, expected=$expected',
|
||||
({ userData, noteableData, expected }) => {
|
||||
Object.assign(state, {
|
||||
userData,
|
||||
noteableData,
|
||||
});
|
||||
|
||||
expect(getters.canUserAddIncidentTimelineEvents(state)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
|
||||
import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
|
||||
import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker';
|
||||
import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
|
||||
import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT, GEMSPEC_FILE_TYPE } from './mock_data';
|
||||
import {
|
||||
PACKAGE_JSON_FILE_TYPE,
|
||||
PACKAGE_JSON_CONTENT,
|
||||
GEMSPEC_FILE_TYPE,
|
||||
GODEPS_JSON_FILE_TYPE,
|
||||
} from './mock_data';
|
||||
|
||||
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
|
||||
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker');
|
||||
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker');
|
||||
|
||||
describe('Highlight.js plugin for linking dependencies', () => {
|
||||
const hljsResultMock = { value: 'test' };
|
||||
|
@ -18,4 +25,9 @@ describe('Highlight.js plugin for linking dependencies', () => {
|
|||
linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE);
|
||||
expect(gemspecLinker).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls godepsJsonLinker for godeps_json file types', () => {
|
||||
linkDependencies(hljsResultMock, GODEPS_JSON_FILE_TYPE);
|
||||
expect(godepsJsonLinker).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,3 +2,5 @@ export const PACKAGE_JSON_FILE_TYPE = 'package_json';
|
|||
export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }';
|
||||
|
||||
export const GEMSPEC_FILE_TYPE = 'gemspec';
|
||||
|
||||
export const GODEPS_JSON_FILE_TYPE = 'godeps_json';
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
|
||||
|
||||
const getInputValue = (dependencyString) =>
|
||||
`<span class="hljs-attr">"ImportPath"</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">"${dependencyString}"</span>`;
|
||||
const getOutputValue = (dependencyString, expectedHref) =>
|
||||
`<span class="hljs-attr">"ImportPath"</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-attr">"<a href="${expectedHref}" rel="nofollow noreferrer noopener">${dependencyString}</a>"</span>`;
|
||||
|
||||
describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
|
||||
it.each`
|
||||
dependency | expectedHref
|
||||
${'gitlab.com/group/project/path'} | ${'https://gitlab.com/group/project/_/tree/master/path'}
|
||||
${'gitlab.com/group/subgroup/project.git/path'} | ${'https://gitlab.com/group/subgroup/_/tree/master/project.git/path'}
|
||||
${'github.com/docker/docker/pkg/homedir'} | ${'https://github.com/docker/docker/tree/master/pkg/homedir'}
|
||||
${'golang.org/x/net/http2'} | ${'https://godoc.org/golang.org/x/net/http2'}
|
||||
${'gopkg.in/yaml.v1'} | ${'https://gopkg.in/yaml.v1'}
|
||||
`(
|
||||
'mutates the input value by wrapping dependency names in anchors and altering path when needed',
|
||||
({ dependency, expectedHref }) => {
|
||||
const inputValue = getInputValue(dependency);
|
||||
const outputValue = getOutputValue(dependency, expectedHref);
|
||||
const hljsResultMock = { value: inputValue };
|
||||
|
||||
const output = godepsJsonLinker(hljsResultMock);
|
||||
expect(output).toBe(outputValue);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1078,10 +1078,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
end
|
||||
|
||||
context 'snowplow stats' do
|
||||
before do
|
||||
stub_feature_flags(usage_data_instrumentation: false)
|
||||
end
|
||||
|
||||
it 'gathers snowplow stats' do
|
||||
expect(subject[:settings][:snowplow_enabled]).to eq(Gitlab::CurrentSettings.snowplow_enabled?)
|
||||
expect(subject[:settings][:snowplow_configured_to_gitlab_collector]).to eq(snowplow_gitlab_host?)
|
||||
|
|
|
@ -264,6 +264,28 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with safe request store', :request_store do
|
||||
it 'changing the flag to true does not affect the current request' do
|
||||
stub_feature_flags(table_rollout_flag => false)
|
||||
|
||||
expect(model.table_name).to eq('_test_ci_jobs_metadata')
|
||||
|
||||
stub_feature_flags(table_rollout_flag => true)
|
||||
|
||||
expect(model.table_name).to eq('_test_ci_jobs_metadata')
|
||||
end
|
||||
|
||||
it 'changing the flag to false does not affect the current request' do
|
||||
stub_feature_flags(table_rollout_flag => true)
|
||||
|
||||
expect(model.table_name).to eq('_test_p_ci_jobs_metadata')
|
||||
|
||||
stub_feature_flags(table_rollout_flag => false)
|
||||
|
||||
expect(model.table_name).to eq('_test_p_ci_jobs_metadata')
|
||||
end
|
||||
end
|
||||
|
||||
def rollout_and_rollback_flag(old, new)
|
||||
# Load class and SQL statements cache
|
||||
old.call
|
||||
|
|
|
@ -8,7 +8,18 @@ RSpec.describe Users::GhostUserMigration do
|
|||
it { is_expected.to belong_to(:initiator_user) }
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:user_id) }
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
describe '.consume_order' do
|
||||
let!(:ghost_user_migration_1) { create(:ghost_user_migration, consume_after: Time.current) }
|
||||
let!(:ghost_user_migration_2) { create(:ghost_user_migration, consume_after: 5.minutes.ago) }
|
||||
|
||||
subject { described_class.consume_order.to_a }
|
||||
|
||||
it { is_expected.to eq([ghost_user_migration_2, ghost_user_migration_1]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,5 +27,34 @@ RSpec.describe Users::MigrateRecordsToGhostUserInBatchesService do
|
|||
|
||||
service.execute
|
||||
end
|
||||
|
||||
it 'process jobs ordered by the consume_after timestamp' do
|
||||
older_ghost_user_migration = create(:ghost_user_migration, user: create(:user),
|
||||
consume_after: 5.minutes.ago)
|
||||
|
||||
# setup execution tracker to only allow a single job to be processed
|
||||
allow_next_instance_of(::Gitlab::Utils::ExecutionTracker) do |tracker|
|
||||
allow(tracker).to receive(:over_limit?).and_return(false, true)
|
||||
end
|
||||
|
||||
expect(Users::MigrateRecordsToGhostUserService).to(
|
||||
receive(:new).with(older_ghost_user_migration.user,
|
||||
older_ghost_user_migration.initiator_user,
|
||||
any_args)
|
||||
).and_call_original
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
it 'reschedules job in case of an error', :freeze_time do
|
||||
expect_next_instance_of(Users::MigrateRecordsToGhostUserService) do |service|
|
||||
expect(service).to(receive(:execute)).and_raise(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
|
||||
expect { service.execute }.to(
|
||||
change { ghost_user_migration.reload.consume_after }
|
||||
.to(30.minutes.from_now))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue