Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-28 12:07:26 +00:00
parent b9e3013993
commit e4dad5d330
45 changed files with 538 additions and 201 deletions

View File

@ -292,13 +292,6 @@ Rails/ApplicationController:
- 'spec/controllers/concerns/continue_params_spec.rb'
- 'spec/lib/marginalia_spec.rb'
# Offense count: 3
# Cop supports --auto-correct.
Rails/BelongsTo:
Exclude:
- 'app/models/deployment.rb'
- 'app/models/environment.rb'
# Offense count: 155
# Cop supports --auto-correct.
Rails/ContentTag:

View File

@ -1 +1 @@
e302aa4a8caf07caad38c236d610fea49a41aa2f
cc42cf8f28dc37bf808dabaac8a055a84b83a5db

View File

@ -44,6 +44,15 @@ export default {
},
},
computed: {
internalValue: {
get() {
return this.value;
},
set(value) {
this.$emit('change', value);
},
},
featureEnabled() {
return this.value !== 0;
},
@ -68,10 +77,6 @@ export default {
this.$emit('change', firstOptionValue);
}
},
selectOption(e) {
this.$emit('change', Number(e.target.value));
},
},
};
</script>
@ -93,15 +98,14 @@ export default {
/>
<div class="select-wrapper gl-flex-grow-1">
<select
v-model="internalValue"
:disabled="displaySelectInput"
class="form-control project-repo-select select-control"
@change="selectOption"
>
<option
v-for="[optionValue, optionName] in displayOptions"
:key="optionValue"
:value="optionValue"
:selected="optionValue === value"
>
{{ optionName }}
</option>

View File

@ -9,7 +9,6 @@ import {
featureAccessLevelMembers,
featureAccessLevelEveryone,
featureAccessLevel,
featureAccessLevelNone,
CVE_ID_REQUEST_BUTTON_I18N,
featureAccessLevelDescriptions,
} from '../constants';
@ -225,8 +224,6 @@ export default {
},
operationsFeatureAccessLevelOptions() {
if (!this.operationsEnabled) return [featureAccessLevelNone];
return this.featureAccessLevelOptions.filter(
([value]) => value <= this.operationsAccessLevel,
);
@ -251,10 +248,6 @@ export default {
return options;
},
metricsOptionsDropdownDisabled() {
return this.operationsFeatureAccessLevelOptions.length < 2 || !this.operationsEnabled;
},
operationsEnabled() {
return this.operationsAccessLevel > featureAccessLevel.NOT_ENABLED;
},
@ -392,6 +385,15 @@ export default {
else if (oldValue === featureAccessLevel.NOT_ENABLED)
toggleHiddenClassBySelector('.merge-requests-feature', false);
},
operationsAccessLevel(value, oldValue) {
if (value < oldValue) {
// sub-features cannot have more permissive access level
this.metricsDashboardAccessLevel = Math.min(this.metricsDashboardAccessLevel, value);
} else if (oldValue === 0) {
this.metricsDashboardAccessLevel = value;
}
},
},
methods: {

View File

@ -34,7 +34,7 @@ export default {
return `${this.severityLabel} - ${this.issue.name}`;
},
issueSeverity() {
return this.issue.severity.toLowerCase();
return this.issue.severity?.toLowerCase();
},
isStatusSuccess() {
return this.status === STATUS_SUCCESS;

View File

@ -38,6 +38,8 @@ module WorkhorseHelper
# Send an entry from artifacts through Workhorse
def send_artifacts_entry(file, entry)
headers.store(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
headers.store(*Gitlab::Workhorse.detect_content_type)
head :ok
end

View File

@ -14,8 +14,8 @@ class Deployment < ApplicationRecord
ARCHIVABLE_OFFSET = 50_000
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :project, optional: false
belongs_to :environment, optional: false
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user
belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations

View File

@ -12,7 +12,7 @@ class Environment < ApplicationRecord
self.reactive_cache_hard_limit = 10.megabytes
self.reactive_cache_work_type = :external_dependency
belongs_to :project, required: true
belongs_to :project, optional: false
use_fast_destroy :all_deployments
nullify_if_blank :external_url

View File

@ -121,6 +121,7 @@ module Projects
# Overridden in EE
def post_update_hooks(project)
move_pages(project)
ensure_personal_project_owner_membership(project)
end
# Overridden in EE
@ -152,6 +153,19 @@ module Projects
project.track_project_repository
end
def ensure_personal_project_owner_membership(project)
# In case of personal projects, we want to make sure that
# a membership record with `OWNER` access level exists for the owner of the namespace.
return unless project.personal?
namespace_owner = project.namespace.owner
existing_membership_record = project.member(namespace_owner)
return if existing_membership_record.present? && existing_membership_record.access_level == Gitlab::Access::OWNER
project.add_owner(namespace_owner)
end
def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.

View File

@ -73,7 +73,6 @@
- internationalization
- jenkins_importer
- kubernetes_management
- license
- license_compliance
- logging
- memory
@ -94,6 +93,7 @@
- privacy_control_center
- product_analytics
- projects
- provision
- purchase
- quality_management
- redis
@ -120,7 +120,6 @@
- subgroups
- team_planning
- tracing
- usage_ping
- users
- utilization
- value_stream_management

View File

@ -1,8 +0,0 @@
---
name: bulk_expire_project_artifacts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75488
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347405
milestone: '14.6'
type: development
group: group::pipeline insights
default_enabled: true

View File

@ -41,7 +41,7 @@ end
Gitlab::Seeder.quiet do
puts "\nGenerating group crm organizations and contacts"
Group.all.find_each do |group|
Group.where('parent_id IS NULL').first(10).each do |group|
Gitlab::Seeder::Crm.new(group).seed!
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class AddIndexesForPrimaryEmailSecondCleanupMigration < Gitlab::Database::Migration[1.0]
USERS_INDEX = :index_users_on_id_for_primary_email_migration
EMAIL_INDEX = :index_emails_on_email_user_id
disable_ddl_transaction!
def up
unless index_exists_by_name?(:users, USERS_INDEX)
disable_statement_timeout do
execute <<~SQL
CREATE INDEX CONCURRENTLY #{USERS_INDEX}
ON users (id) INCLUDE (email, confirmed_at)
WHERE confirmed_at IS NOT NULL
SQL
end
end
add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
end
def down
remove_concurrent_index_by_name :users, USERS_INDEX
remove_concurrent_index_by_name :emails, EMAIL_INDEX
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
class CleanupAfterFixingIssueWhenAdminChangedPrimaryEmail < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
BATCH_SIZE = 10_000
# Stubbed class to access the User table
class User < ActiveRecord::Base
include ::EachBatch
self.table_name = 'users'
self.inheritance_column = :_type_disabled
scope :confirmed, -> { where.not(confirmed_at: nil) }
has_many :emails
end
# Stubbed class to access the Emails table
class Email < ActiveRecord::Base
self.table_name = 'emails'
self.inheritance_column = :_type_disabled
belongs_to :user
end
def up
# Select confirmed users that do not have their primary email in the emails table,
# and create the email record.
not_exists_condition = 'NOT EXISTS (SELECT 1 FROM emails WHERE emails.email = users.email AND emails.user_id = users.id)'
User.confirmed.each_batch(of: BATCH_SIZE) do |user_batch|
user_batch.select(:id, :email, :confirmed_at).where(not_exists_condition).each do |user|
current_time = Time.now.utc
begin
Email.create(
user_id: user.id,
email: user.email,
confirmed_at: user.confirmed_at,
created_at: current_time,
updated_at: current_time
)
rescue StandardError => error
Gitlab::AppLogger.error("Could not add primary email #{user.email} to emails for user with ID #{user.id} due to #{error}")
end
end
end
end
def down
# Intentionally left blank
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class DropTemporaryIndexesForPrimaryEmailMigrationSecondCleanup < Gitlab::Database::Migration[1.0]
USERS_INDEX = :index_users_on_id_for_primary_email_migration
EMAIL_INDEX = :index_emails_on_email_user_id
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :users, USERS_INDEX
remove_concurrent_index_by_name :emails, EMAIL_INDEX
end
def down
unless index_exists_by_name?(:users, USERS_INDEX)
disable_statement_timeout do
execute <<~SQL
CREATE INDEX CONCURRENTLY #{USERS_INDEX}
ON users (id) INCLUDE (email, confirmed_at)
WHERE confirmed_at IS NOT NULL
SQL
end
end
add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
end
end

View File

@ -0,0 +1 @@
5da6020c9e4cca8659b45393812ee4d76f6e9422803acaadd8c1b046be8c647a

View File

@ -0,0 +1 @@
748ab129352d12d40e5d97dfb8a658ff2d62642e9f5cb1deb19ed871328f9d07

View File

@ -0,0 +1 @@
416ff5e57b2b13ccb55c6f1e88e6b0603dfc086a8a15be810752a9449ed4f3a1

View File

@ -628,6 +628,8 @@ Input type: `AdminSidekiqQueuesDeleteJobsInput`
| <a id="mutationadminsidekiqqueuesdeletejobsclientid"></a>`clientId` | [`String`](#string) | Delete jobs matching client_id in the context metadata. |
| <a id="mutationadminsidekiqqueuesdeletejobsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationadminsidekiqqueuesdeletejobsfeaturecategory"></a>`featureCategory` | [`String`](#string) | Delete jobs matching feature_category in the context metadata. |
| <a id="mutationadminsidekiqqueuesdeletejobsjobid"></a>`jobId` | [`String`](#string) | Delete jobs matching job_id in the context metadata. |
| <a id="mutationadminsidekiqqueuesdeletejobspipelineid"></a>`pipelineId` | [`String`](#string) | Delete jobs matching pipeline_id in the context metadata. |
| <a id="mutationadminsidekiqqueuesdeletejobsproject"></a>`project` | [`String`](#string) | Delete jobs matching project in the context metadata. |
| <a id="mutationadminsidekiqqueuesdeletejobsqueuename"></a>`queueName` | [`String!`](#string) | Name of the queue to delete jobs from. |
| <a id="mutationadminsidekiqqueuesdeletejobsrelatedclass"></a>`relatedClass` | [`String`](#string) | Delete jobs matching related_class in the context metadata. |

View File

@ -287,11 +287,8 @@ If the artifacts were deleted successfully, a response with status `204 No Conte
## Delete project artifacts
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223793) in GitLab 14.7 [with a flag](../administration/feature_flags.md) named `bulk_expire_project_artifacts`. Enabled by default on GitLab self-managed. Enabled on GitLab.com.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to
[disable the `bulk_expire_project_artifacts` flag](../administration/feature_flags.md). On GitLab.com, this feature is available.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223793) in GitLab 14.7 [with a flag](../administration/feature_flags.md) named `bulk_expire_project_artifacts`. Enabled by default on GitLab self-managed. Enabled on GitLab.com.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/350609) in GitLab 14.10.
Delete artifacts of a project that can be deleted.

View File

@ -1,19 +1,11 @@
---
stage: Create
group: Source Code
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"
type: howto
redirect_to: '../tutorials/make_your_first_git_commit.md'
remove_date: '2022-06-26'
---
# How to create a branch **(FREE)**
This document was moved to [another location](../tutorials/make_your_first_git_commit.md).
A branch is an independent line of development in a [project](../user/project/index.md).
When you create a branch (in your [terminal](start-using-git.md#create-a-branch) or with
[the web interface](../user/project/repository/web_editor.md#create-a-new-branch)),
you are creating a snapshot of a certain branch, usually the main branch,
at its current state. From there, you can start to make your own changes without
affecting the main codebase. The history of your changes is tracked in your branch.
When your changes are ready, you then merge them into the rest of the codebase with a
[merge request](../user/project/merge_requests/creating_merge_requests.md).
<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -6,7 +6,7 @@ type: howto, tutorial
description: "Introduction to using Git through the command line."
---
# Start using Git on the command line **(FREE)**
# Git on the command line **(FREE)**
[Git](https://git-scm.com/) is an open-source distributed version control system. GitLab is built
on top of Git.
@ -14,6 +14,9 @@ on top of Git.
You can do many Git operations directly in GitLab. However, the command line is required for advanced tasks,
like fixing complex merge conflicts or rolling back commits.
If you're new to Git and want to learn by working in your own project,
[learn how to make your first commit](../tutorials/make_your_first_git_commit.md).
For a quick reference of Git commands, download a [Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf).
For more information about the advantages of working with Git and GitLab:
@ -24,75 +27,7 @@ For more information about the advantages of working with Git and GitLab:
To help you visualize what you're doing locally, you can install a
[Git GUI app](https://git-scm.com/download/gui/).
## Git terminology
If you're familiar with Git terminology, you might want to skip this section and
go directly to [prerequisites](#prerequisites).
### Repository
In GitLab, files are stored in a **repository**. A repository is similar to how you
store files in a folder or directory on your computer.
- A **remote repository** refers to the files in GitLab.
- A **local copy** refers to the files on your computer.
<!-- vale gitlab.Spelling = NO -->
<!-- vale gitlab.SubstitutionWarning = NO -->
Often, the word "repository" is shortened to "repo".
<!-- vale gitlab.Spelling = YES -->
<!-- vale gitlab.SubstitutionWarning = YES -->
In GitLab, a repository is contained in a **project**.
### Fork
When you want to contribute to someone else's repository, you make a copy of it.
This copy is called a [**fork**](../user/project/repository/forking_workflow.md#creating-a-fork).
The process is called "creating a fork."
When you fork a repo, you create a copy of the project in your own
[namespace](../user/group/#namespaces). You then have write permissions to modify the project files
and settings.
For example, you can fork this project, <https://gitlab.com/gitlab-tests/sample-project/>, into your namespace.
You now have your own copy of the repository. You can view the namespace in the URL, for example
`https://gitlab.com/your-namespace/sample-project/`.
Then you can clone the repository to your local machine, work on the files, and submit changes back to the
original repository.
### Difference between download and clone
To create a copy of a remote repository's files on your computer, you can either
**download** or **clone** the repository. If you download it, you cannot sync the repository with the
remote repository on GitLab.
[Cloning](#clone-a-repository) a repository is the same as downloading, except it preserves the Git connection
with the remote repository. You can then modify the files locally and
upload the changes to the remote repository on GitLab.
### Pull and push
After you save a local copy of a repository and modify the files on your computer, you can upload the
changes to GitLab. This is referred to as **pushing** to the remote, because you use the command
[`git push`](#send-changes-to-gitlabcom).
When the remote repository changes, your local copy is behind. You can update your local copy with the new
changes in the remote repository.
This is referred to as **pulling** from the remote, because you use the command
[`git pull`](#download-the-latest-changes-in-the-project).
## Prerequisites
To start using GitLab with Git, complete the following tasks:
- Create and sign in to a GitLab account.
- [Open a terminal](#open-a-terminal).
- [Install Git](#install-git) on your computer.
- [Configure Git](#configure-git).
- [Choose a repository](#choose-a-repository).
### Open a terminal
## Choose a terminal
To execute Git commands on your computer, you must open a terminal (also known as command
prompt, command shell, and command line). Here are some options:
@ -107,9 +42,9 @@ prompt, command shell, and command line). Here are some options:
- For Linux users:
- Built-in [Linux Terminal](https://ubuntu.com/tutorials/command-line-for-beginners#3-opening-a-terminal).
### Install Git
## Confirm Git is installed
Determine if Git is already installed on your computer by opening a terminal
You can determine if Git is already installed on your computer by opening a terminal
and running this command:
```shell
@ -123,9 +58,8 @@ git version X.Y.Z
```
If your computer doesn't recognize `git` as a command, you must [install Git](../topics/git/how_to_install_git/index.md).
After you install Git, run `git --version` to confirm that it installed correctly.
### Configure Git
## Configure Git
To start using Git from your computer, you must enter your credentials
to identify yourself as the author of your work. The username and email address
@ -156,7 +90,7 @@ should match the ones you use in GitLab.
You can read more on how Git manages configurations in the
[Git configuration documentation](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration).
### Choose a repository
## Choose a repository
Before you begin, choose the repository you want to work in. You can use any project you have permission to
access on GitLab.com or any other GitLab instance.

View File

@ -26,10 +26,11 @@ The following resources can help you get started with Git:
- [Git-ing started with Git](https://www.youtube.com/watch?v=Ce5nz5n41z4),
a video introduction to Git.
- [Make your first Git commit](../../tutorials/make_your_first_git_commit.md)
- [Git Basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics)
- [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab)
- [How to install Git](how_to_install_git/index.md)
- [Git terminology](../../gitlab-basics/start-using-git.md#git-terminology)
- [Git terminology](terminology.md)
- [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
- [Edit files through the command line](../../gitlab-basics/command-line-commands.md)
- [GitLab Git Cheat Sheet (download)](https://about.gitlab.com/images/press/git-cheat-sheet.pdf)

View File

@ -0,0 +1,62 @@
---
stage: Create
group: Source Code
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
---
# Git terminology
The following are commonly-used Git terms.
## Repository
In GitLab, files are stored in a **repository**. A repository is similar to how you
store files in a folder or directory on your computer.
- A **remote repository** refers to the files in GitLab.
- A **local copy** refers to the files on your computer.
<!-- vale gitlab.Spelling = NO -->
<!-- vale gitlab.SubstitutionWarning = NO -->
Often, the word "repository" is shortened to "repo".
<!-- vale gitlab.Spelling = YES -->
<!-- vale gitlab.SubstitutionWarning = YES -->
In GitLab, a repository is contained in a **project**.
## Fork
When you want to contribute to someone else's repository, you make a copy of it.
This copy is called a [**fork**](../../user/project/repository/forking_workflow.md#creating-a-fork).
The process is called "creating a fork."
When you fork a repo, you create a copy of the project in your own
[namespace](../../user/group/#namespaces). You then have write permissions to modify the project files
and settings.
For example, you can fork this project, <https://gitlab.com/gitlab-tests/sample-project/>, into your namespace.
You now have your own copy of the repository. You can view the namespace in the URL, for example
`https://gitlab.com/your-namespace/sample-project/`.
Then you can clone the repository to your local machine, work on the files, and submit changes back to the
original repository.
## Difference between download and clone
To create a copy of a remote repository's files on your computer, you can either
**download** or **clone** the repository. If you download it, you cannot sync the repository with the
remote repository on GitLab.
[Cloning](../../gitlab-basics/start-using-git.md#clone-a-repository) a repository is the same as downloading, except it preserves the Git connection
with the remote repository. You can then modify the files locally and
upload the changes to the remote repository on GitLab.
## Pull and push
After you save a local copy of a repository and modify the files on your computer, you can upload the
changes to GitLab. This is referred to as **pushing** to the remote, because you use the command
[`git push`](../../gitlab-basics/start-using-git.md#send-changes-to-gitlabcom).
When the remote repository changes, your local copy is behind. You can update your local copy with the new
changes in the remote repository.
This is referred to as **pulling** from the remote, because you use the command
[`git pull`](../../gitlab-basics/start-using-git.md#download-the-latest-changes-in-the-project).

View File

@ -250,7 +250,7 @@ Let's look in the UI and confirm your changes. Go to your project.
- Scroll down and view the contents of the `README.md` file.
Your changes should be visible.
- Above the `README.md` file, view the text in the `Last commit` column.
- Above the `README.md` file, view the text in the **Last commit** column.
Your commit message is displayed in this column:
![Commit message](img/commit_message_v14_10.png)

View File

@ -52,6 +52,9 @@ To view vulnerabilities in a pipeline:
1. From the list, select the pipeline you want to check for vulnerabilities.
1. Select the **Security** tab.
**Scan details** shows vulnerabilities introduced by the merge request, in addition to existing vulnerabilities
from the latest successful pipeline in your project's default branch.
A pipeline consists of multiple jobs, such as SAST and DAST scans. If a job fails to finish,
the security dashboard doesn't show SAST scanner output. For example, if the SAST
job finishes but the DAST job fails, the security dashboard doesn't show SAST results. On failure,
@ -66,7 +69,8 @@ To view the total number of vulnerabilities per scan:
1. Select the **Status** of a branch.
1. Select the **Security** tab.
**Scan details** show the total number of vulnerabilities found per scan in the pipeline.
**Scan details** shows vulnerabilities introduced by the merge request, in addition to existing vulnerabilities
from the latest successful pipeline in your project's default branch.
### Download security scan outputs

View File

@ -245,26 +245,26 @@ epics:
| Event | Sent to |
|------------------------|---------|
| Change milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected. |
| Change milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected. |
| Change milestone issue | Subscribers and participants mentioned. |
| Change milestone merge request | Subscribers and participants mentioned. |
| Close epic | |
| Close issue | |
| Close merge request | |
| Due issue | Participants and Custom notification level with this event selected. |
| Failed pipeline | The author of the pipeline. |
| Fixed pipeline | The author of the pipeline. Enabled by default. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24309) in GitLab 13.1._ |
| Issue due | Participants and Custom notification level with this event selected. |
| Merge merge request | |
| Merge when pipeline succeeds | Author, Participants, Watchers, Subscribers, and Custom notification level with this event selected. Custom notification level is ignored for Author, Watchers and Subscribers. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211961) in GitLab 13.4._ |
| Merge request [marked as ready](../project/merge_requests/drafts.md) | Watchers and participants. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15332) in GitLab 13.10._ |
| New comment | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
| New epic | |
| New issue | |
| New merge request | |
| New note | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
| Push to merge request | Participants and Custom notification level with this event selected. |
| Reassign issue | Participants, Watchers, Subscribers, Custom notification level with this event selected, and the old assignee. |
| Reassign merge request | Participants, Watchers, Subscribers, Custom notification level with this event selected, and the old assignee. |
| Remove milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected. |
| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected. |
| Remove milestone issue | Subscribers and participants mentioned. |
| Remove milestone merge request | Subscribers and participants mentioned. |
| Reopen epic | |
| Reopen issue | |
| Reopen merge request | |

View File

@ -104,10 +104,7 @@ module API
def set_application_context
return unless current_job
Gitlab::ApplicationContext.push(
user: -> { current_job.user },
project: -> { current_job.project }
)
Gitlab::ApplicationContext.push(job: current_job)
end
def track_ci_minutes_usage!(_build, _runner)

View File

@ -140,8 +140,6 @@ module API
desc 'Expire the artifacts files from a project'
delete ':id/artifacts' do
not_found! unless Feature.enabled?(:bulk_expire_project_artifacts, default_enabled: :yaml)
authorize_destroy_artifacts!
::Ci::JobArtifacts::DeleteProjectArtifactsService.new(project: user_project).execute

View File

@ -707,6 +707,7 @@ module API
def send_artifacts_entry(file, entry)
header(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
header(*Gitlab::Workhorse.detect_content_type)
body ''
end

View File

@ -16,6 +16,8 @@ module Gitlab
:client_id,
:caller_id,
:remote_ip,
:job_id,
:pipeline_id,
:related_class,
:feature_category
].freeze
@ -28,6 +30,7 @@ module Gitlab
Attribute.new(:runner, ::Ci::Runner),
Attribute.new(:caller_id, String),
Attribute.new(:remote_ip, String),
Attribute.new(:job, ::Ci::Build),
Attribute.new(:related_class, String),
Attribute.new(:feature_category, String)
].freeze
@ -73,14 +76,16 @@ module Gitlab
def to_lazy_hash
{}.tap do |hash|
hash[:user] = -> { username } if set_values.include?(:user)
hash[:project] = -> { project_path } if set_values.include?(:project) || set_values.include?(:runner)
hash[:user] = -> { username } if include_user?
hash[:project] = -> { project_path } if include_project?
hash[:root_namespace] = -> { root_namespace_path } if include_namespace?
hash[:client_id] = -> { client } if include_client?
hash[:caller_id] = caller_id if set_values.include?(:caller_id)
hash[:remote_ip] = remote_ip if set_values.include?(:remote_ip)
hash[:related_class] = related_class if set_values.include?(:related_class)
hash[:feature_category] = feature_category if set_values.include?(:feature_category)
hash[:pipeline_id] = -> { job&.pipeline_id } if set_values.include?(:job)
hash[:job_id] = -> { job&.id } if set_values.include?(:job)
end
end
@ -103,32 +108,41 @@ module Gitlab
end
def project_path
associated_routable = project || runner_project
associated_routable = project || runner_project || job_project
associated_routable&.full_path
end
def username
user&.username
associated_user = user || job_user
associated_user&.username
end
def root_namespace_path
associated_routable = namespace || project || runner_project || runner_group
associated_routable = namespace || project || runner_project || runner_group || job_project
associated_routable&.full_path_components&.first
end
def include_namespace?
set_values.include?(:namespace) || set_values.include?(:project) || set_values.include?(:runner)
set_values.include?(:namespace) || set_values.include?(:project) || set_values.include?(:runner) || set_values.include?(:job)
end
def include_client?
set_values.include?(:user) || set_values.include?(:runner) || set_values.include?(:remote_ip)
end
def include_user?
set_values.include?(:user) || set_values.include?(:job)
end
def include_project?
set_values.include?(:project) || set_values.include?(:runner) || set_values.include?(:job)
end
def client
if user
"user/#{user.id}"
elsif runner
if runner
"runner/#{runner.id}"
elsif user
"user/#{user.id}"
else
"ip/#{remote_ip}"
end
@ -150,6 +164,18 @@ module Gitlab
runner.groups.first
end
end
def job_project
strong_memoize(:job_project) do
job&.project
end
end
def job_user
strong_memoize(:job_user) do
job&.user
end
end
end
end

View File

@ -226,6 +226,13 @@ module Gitlab
end
end
def detect_content_type
[
Gitlab::Workhorse::DETECT_HEADER,
'true'
]
end
protected
# This is the outermost encoding of a senddata: header. It is safe for

View File

@ -323,6 +323,7 @@ RSpec.describe Projects::ArtifactsController do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Gitlab-Workhorse-Detect-Content-Type']).to eq('true')
expect(send_data).to start_with('artifacts-entry:')
expect(params.keys).to eq(%w(Archive Entry))

View File

@ -3,15 +3,17 @@ import { shallowMount } from '@vue/test-utils';
import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
describe('Project Feature Settings', () => {
const defaultOptions = [
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
];
const defaultProps = {
name: 'Test',
options: [
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
],
options: defaultOptions,
value: 1,
disabledInput: false,
showToggle: true,
@ -110,15 +112,25 @@ describe('Project Feature Settings', () => {
},
);
it('should emit the change when a new option is selected', () => {
it('should emit the change when a new option is selected', async () => {
wrapper = mountComponent();
expect(wrapper.emitted('change')).toBeUndefined();
wrapper.findAll('option').at(1).trigger('change');
await wrapper.findAll('option').at(1).setSelected();
expect(wrapper.emitted('change')).toHaveLength(1);
expect(wrapper.emitted('change')[0]).toEqual([2]);
});
it('value of select matches prop `value` if options are modified', async () => {
wrapper = mountComponent();
await wrapper.setProps({ value: 0, options: [[0, 0]] });
expect(wrapper.find('select').element.selectedIndex).toBe(0);
await wrapper.setProps({ value: 2, options: defaultOptions });
expect(wrapper.find('select').element.selectedIndex).toBe(1);
});
});
});

View File

@ -1,6 +1,6 @@
import { GlSprintf, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
import {
featureAccessLevel,
@ -21,6 +21,7 @@ const defaultProps = {
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
operationsAccessLevel: 20,
metricsDashboardAccessLevel: 20,
pagesAccessLevel: 10,
analyticsAccessLevel: 20,
containerRegistryAccessLevel: 20,
@ -75,7 +76,7 @@ describe('Settings Panel', () => {
const findLFSFeatureToggle = () => findLFSSettingsRow().find(GlToggle);
const findRepositoryFeatureProjectRow = () => wrapper.find({ ref: 'repository-settings' });
const findRepositoryFeatureSetting = () =>
findRepositoryFeatureProjectRow().find(projectFeatureSetting);
findRepositoryFeatureProjectRow().find(ProjectFeatureSetting);
const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' });
const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' });
const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' });
@ -106,7 +107,11 @@ describe('Settings Panel', () => {
'input[name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"]',
);
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
const findMetricsVisibilityInput = () =>
findMetricsVisibilitySettings().findComponent(ProjectFeatureSetting);
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
const findOperationsVisibilityInput = () =>
findOperationsSettings().findComponent(ProjectFeatureSetting);
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
afterEach(() => {
@ -595,7 +600,7 @@ describe('Settings Panel', () => {
});
describe('Metrics dashboard', () => {
it('should show the metrics dashboard access toggle', () => {
it('should show the metrics dashboard access select', () => {
wrapper = mountComponent();
expect(findMetricsVisibilitySettings().exists()).toBe(true);
@ -610,23 +615,51 @@ describe('Settings Panel', () => {
});
it.each`
scenario | selectedOption | selectedOptionLabel
${{ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }} | ${String(featureAccessLevel.PROJECT_MEMBERS)} | ${'Only Project Members'}
${{ currentSettings: { operationsAccessLevel: featureAccessLevel.NOT_ENABLED } }} | ${String(featureAccessLevel.NOT_ENABLED)} | ${'Enable feature to choose access level'}
before | after
${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.EVERYONE}
${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.PROJECT_MEMBERS}
${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
${featureAccessLevel.EVERYONE} | ${featureAccessLevel.NOT_ENABLED}
${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.NOT_ENABLED}
`(
'should disable the metrics visibility dropdown when #scenario',
({ scenario, selectedOption, selectedOptionLabel }) => {
wrapper = mountComponent(scenario, mount);
'when updating Operations Settings access level from `$before` to `$after`, Metric Dashboard access is updated to `$after` as well',
async ({ before, after }) => {
wrapper = mountComponent({
currentSettings: { operationsAccessLevel: before, metricsDashboardAccessLevel: before },
});
const select = findMetricsVisibilitySettings().find('select');
const option = select.find('option');
await findOperationsVisibilityInput().vm.$emit('change', after);
expect(select.attributes('disabled')).toBe('disabled');
expect(select.element.value).toBe(selectedOption);
expect(option.attributes('value')).toBe(selectedOption);
expect(option.text()).toBe(selectedOptionLabel);
expect(findMetricsVisibilityInput().props('value')).toBe(after);
},
);
it('when updating Operations Settings access level from `10` to `20`, Metric Dashboard access is not increased', async () => {
wrapper = mountComponent({
currentSettings: {
operationsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
},
});
await findOperationsVisibilityInput().vm.$emit('change', featureAccessLevel.EVERYONE);
expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
});
it('should reduce Metrics visibility level when visibility is set to private', async () => {
wrapper = mountComponent({
currentSettings: {
visibilityLevel: visibilityOptions.PUBLIC,
operationsAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.EVERYONE,
},
});
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
});
});
describe('Analytics', () => {

View File

@ -51,6 +51,7 @@ describe('code quality issue body issue body', () => {
${'blocker'} | ${'text-danger-800'} | ${'severity-critical'}
${'unknown'} | ${'text-secondary-400'} | ${'severity-unknown'}
${'invalid'} | ${'text-secondary-400'} | ${'severity-unknown'}
${undefined} | ${'text-secondary-400'} | ${'severity-unknown'}
`(
'renders correct icon for "$severity" severity rating',
({ severity, iconClass, iconName }) => {

View File

@ -146,7 +146,8 @@ RSpec.describe Gitlab::ApplicationContext do
where(:provided_options, :client) do
[:remote_ip] | :remote_ip
[:remote_ip, :runner] | :runner
[:remote_ip, :runner, :user] | :user
[:remote_ip, :runner, :user] | :runner
[:remote_ip, :user] | :user
end
with_them do
@ -195,6 +196,16 @@ RSpec.describe Gitlab::ApplicationContext do
expect(result(context)).to include(project: nil)
end
end
context 'when using job context' do
let_it_be(:job) { create(:ci_build, :pending, :queued, user: user, project: project) }
it 'sets expected values' do
context = described_class.new(job: job)
expect(result(context)).to include(job_id: job.id, project: project.full_path, pipeline_id: job.pipeline_id)
end
end
end
describe '#use' do

View File

@ -448,6 +448,14 @@ RSpec.describe Gitlab::Workhorse do
end
end
describe '.detect_content_type' do
subject { described_class.detect_content_type }
it 'returns array setting detect content type in workhorse' do
expect(subject).to eq(%w[Gitlab-Workhorse-Detect-Content-Type true])
end
end
describe '.send_git_blob' do
include FakeBlobHelpers

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe CleanupAfterFixingIssueWhenAdminChangedPrimaryEmail, :sidekiq do
let(:migration) { described_class.new }
let(:users) { table(:users) }
let(:emails) { table(:emails) }
let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 3.days.ago, projects_limit: 100) }
let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
let!(:user_4) { users.create!(name: 'unconfirmed-user', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) }
let!(:email_1) { emails.create!(email: 'confirmed-1@example.com', user_id: user_1.id, confirmed_at: 1.day.ago) }
let!(:email_2) { emails.create!(email: 'other_2@example.com', user_id: user_2.id, confirmed_at: 1.day.ago) }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
end
it 'adds the primary email to emails for leftover confirmed users that do not have their primary email in the emails table', :aggregate_failures do
original_email_1_confirmed_at = email_1.reload.confirmed_at
expect { migration.up }.to change { emails.count }.by(2)
expect(emails.find_by(user_id: user_2.id, email: 'confirmed-2@example.com').confirmed_at).to eq(user_2.reload.confirmed_at)
expect(emails.find_by(user_id: user_3.id, email: 'confirmed-3@example.com').confirmed_at).to eq(user_3.reload.confirmed_at)
expect(email_1.reload.confirmed_at).to eq(original_email_1_confirmed_at)
expect(emails.exists?(user_id: user_4.id)).to be(false)
end
it 'continues in case of errors with one email' do
allow(Email).to receive(:create) { raise 'boom!' }
expect { migration.up }.not_to raise_error
end
end

View File

@ -82,18 +82,6 @@ RSpec.describe API::Ci::JobArtifacts do
end
describe 'DELETE /projects/:id/artifacts' do
context 'when feature flag is disabled' do
before do
stub_feature_flags(bulk_expire_project_artifacts: false)
end
it 'returns 404' do
delete api("/projects/#{project.id}/artifacts", api_user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user is anonymous' do
let(:api_user) { nil }
@ -568,7 +556,8 @@ RSpec.describe API::Ci::JobArtifacts do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
'Gitlab-Workhorse-Detect-Content-Type' => 'true')
end
end
@ -638,7 +627,8 @@ RSpec.describe API::Ci::JobArtifacts do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
'Gitlab-Workhorse-Detect-Content-Type' => 'true')
expect(response.parsed_body).to be_empty
end
end
@ -656,7 +646,8 @@ RSpec.describe API::Ci::JobArtifacts do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
'Gitlab-Workhorse-Detect-Content-Type' => 'true')
end
end

View File

@ -853,11 +853,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
subject { request_job(id: job.id) }
it_behaves_like 'storing arguments in the application context for the API' do
let(:expected_params) { { user: user.username, project: project.full_path, client_id: "user/#{user.id}" } }
let(:expected_params) { { user: user.username, project: project.full_path, client_id: "runner/#{runner.id}", job_id: job.id, pipeline_id: job.pipeline_id } }
end
it_behaves_like 'not executing any extra queries for the application context', 3 do
# Extra queries: User, Project, Route
it_behaves_like 'not executing any extra queries for the application context', 4 do
# Extra queries: User, Project, Route, Runner
let(:subject_proc) { proc { request_job(id: job.id) } }
end
end

View File

@ -11,8 +11,9 @@ RSpec.describe Projects::TransferService do
let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) }
let(:target) { group }
let(:executor) { user }
subject(:execute_transfer) { described_class.new(project, user).execute(target).tap { project.reload } }
subject(:execute_transfer) { described_class.new(project, executor).execute(target).tap { project.reload } }
context 'with npm packages' do
before do
@ -92,6 +93,55 @@ RSpec.describe Projects::TransferService do
end
end
context 'project in a group -> a personal namespace', :enable_admin_mode do
let(:project) { create(:project, :repository, :legacy_storage, group: group) }
let(:target) { user.namespace }
# We need to use an admin user as the executor because
# only an admin user has required permissions to transfer projects
# under _all_ the different circumstances specified below.
let(:executor) { create(:user, :admin) }
it 'executes the transfer to personal namespace successfully' do
execute_transfer
expect(project.namespace).to eq(user.namespace)
end
context 'the owner of the namespace does not have a direct membership in the project residing in the group' do
it 'creates a project membership record for the owner of the namespace, with OWNER access level, after the transfer' do
execute_transfer
expect(project.members.owners.find_by(user_id: user.id)).to be_present
end
end
context 'the owner of the namespace has a direct membership in the project residing in the group' do
context 'that membership has an access level of OWNER' do
before do
project.add_owner(user)
end
it 'retains the project membership record for the owner of the namespace, with OWNER access level, after the transfer' do
execute_transfer
expect(project.members.owners.find_by(user_id: user.id)).to be_present
end
end
context 'that membership has an access level that is not OWNER' do
before do
project.add_developer(user)
end
it 'updates the project membership record for the owner of the namespace, to OWNER access level, after the transfer' do
execute_transfer
expect(project.members.owners.find_by(user_id: user.id)).to be_present
end
end
end
end
context 'when transfer succeeds' do
before do
group.add_owner(user)

View File

@ -19,6 +19,7 @@ type Proxy struct {
reverseProxy *httputil.ReverseProxy
AllowResponseBuffering bool
customHeaders map[string]string
forceTargetHostHeader bool
}
func WithCustomHeaders(customHeaders map[string]string) func(*Proxy) {
@ -27,6 +28,12 @@ func WithCustomHeaders(customHeaders map[string]string) func(*Proxy) {
}
}
func WithForcedTargetHostHeader() func(*Proxy) {
return func(proxy *Proxy) {
proxy.forceTargetHostHeader = true
}
}
func NewProxy(myURL *url.URL, version string, roundTripper http.RoundTripper, options ...func(*Proxy)) *Proxy {
p := Proxy{Version: version, AllowResponseBuffering: true, customHeaders: make(map[string]string)}
@ -43,6 +50,17 @@ func NewProxy(myURL *url.URL, version string, roundTripper http.RoundTripper, op
option(&p)
}
if p.forceTargetHostHeader {
// because of https://github.com/golang/go/issues/28168, the
// upstream won't receive the expected Host header unless this
// is forced in the Director func here
previousDirector := p.reverseProxy.Director
p.reverseProxy.Director = func(request *http.Request) {
previousDirector(request)
request.Host = request.URL.Host
}
}
return &p
}

View File

@ -243,6 +243,7 @@ func (u *upstream) updateGeoProxyFields(geoProxyURL *url.URL) {
u.Version,
geoProxyRoundTripper,
proxypkg.WithCustomHeaders(geoProxyWorkhorseHeaders),
proxypkg.WithForcedTargetHostHeader(),
)
u.geoProxyCableRoute = u.wsRoute(`^/-/cable\z`, geoProxyUpstream)
u.geoProxyRoute = u.route("", "", geoProxyUpstream, withGeoProxy())

View File

@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"testing"
"time"
@ -31,10 +32,15 @@ func newProxy(url string, rt http.RoundTripper, opts ...func(*proxy.Proxy)) *pro
}
func TestProxyRequest(t *testing.T) {
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) {
inboundURL, err := url.Parse("https://explicitly.set.host/url/path")
require.NoError(t, err, "parse inbound url")
urlRegexp := regexp.MustCompile(fmt.Sprintf(`%s\z`, inboundURL.Path))
ts := testhelper.TestServerWithHandler(urlRegexp, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method, "method")
require.Equal(t, "test", r.Header.Get("Custom-Header"), "custom header")
require.Equal(t, testVersion, r.Header.Get("Gitlab-Workhorse"), "version header")
require.Equal(t, inboundURL.Host, r.Host, "sent host header")
require.Regexp(
t,
@ -52,7 +58,7 @@ func TestProxyRequest(t *testing.T) {
fmt.Fprint(w, "RESPONSE")
})
httpRequest, err := http.NewRequest("POST", ts.URL+"/url/path", bytes.NewBufferString("REQUEST"))
httpRequest, err := http.NewRequest("POST", inboundURL.String(), bytes.NewBufferString("REQUEST"))
require.NoError(t, err)
httpRequest.Header.Set("Custom-Header", "test")
@ -64,6 +70,30 @@ func TestProxyRequest(t *testing.T) {
require.Equal(t, "test", w.Header().Get("Custom-Response-Header"), "custom response header")
}
func TestProxyWithForcedTargetHostHeader(t *testing.T) {
var tsUrl *url.URL
inboundURL, err := url.Parse("https://explicitly.set.host/url/path")
require.NoError(t, err, "parse upstream url")
urlRegexp := regexp.MustCompile(fmt.Sprintf(`%s\z`, inboundURL.Path))
ts := testhelper.TestServerWithHandler(urlRegexp, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, tsUrl.Host, r.Host, "upstream host header")
_, err := w.Write([]byte(`ok`))
require.NoError(t, err, "write ok response")
})
tsUrl, err = url.Parse(ts.URL)
require.NoError(t, err, "parse testserver URL")
httpRequest, err := http.NewRequest("POST", inboundURL.String(), nil)
require.NoError(t, err)
w := httptest.NewRecorder()
testProxy := newProxy(ts.URL, nil, proxy.WithForcedTargetHostHeader())
testProxy.ServeHTTP(w, httpRequest)
testhelper.RequireResponseBody(t, w, "ok")
}
func TestProxyWithCustomHeaders(t *testing.T) {
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "value", r.Header.Get("Custom-Header"), "custom proxy header")