Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-02 15:11:07 +00:00
parent ed50918678
commit e36443c1d6
35 changed files with 495 additions and 71 deletions

View File

@ -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'

View File

@ -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"},

View File

@ -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)

View File

@ -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,
);
};

View File

@ -99,7 +99,6 @@ export const LEGACY_FILE_TYPES = [
'podspec',
'podspec_json',
'cartfile',
'godeps_json',
'requirements_txt',
'cargo_toml',
'go_mod',

View File

@ -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,
};
/**

View File

@ -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 = '&quot;</span>';
const importPathString =
'ImportPath&quot;</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">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;github.com/ayufan/golang-kardianos-service&quot;</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));
};

View File

@ -1,6 +1,4 @@
.user-contrib-cell {
stroke: $t-gray-a-08;
&:hover {
cursor: pointer;
stroke: $black;

View File

@ -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;
/*

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
c3a38f280c8835e77953b69ba41ef5d58b76fd5f2f39e758a523c493306b0ab2

View File

@ -0,0 +1 @@
77aca033a7c58af4e981136b96629acf5b82a42701072928532681dd91b05280

View File

@ -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);

View File

@ -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 |
| ---- | ---- | -------- | ----------- |

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,5 +5,6 @@ FactoryBot.define do
association :user
initiator_user { association(:user) }
hard_delete { false }
consume_after { Time.current }
end
end

View File

@ -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');
});
});
});
});

View File

@ -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);
},
);
});
});

View File

@ -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();
});
});

View File

@ -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';

View File

@ -0,0 +1,27 @@
import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
const getInputValue = (dependencyString) =>
`<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;${dependencyString}&quot;</span>`;
const getOutputValue = (dependencyString, expectedHref) =>
`<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-attr">&quot;<a href="${expectedHref}" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</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);
},
);
});

View File

@ -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?)

View File

@ -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

View File

@ -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

View File

@ -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