Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b9e3013993
commit
e4dad5d330
|
@ -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:
|
||||
|
|
|
@ -1 +1 @@
|
|||
e302aa4a8caf07caad38c236d610fea49a41aa2f
|
||||
cc42cf8f28dc37bf808dabaac8a055a84b83a5db
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
5da6020c9e4cca8659b45393812ee4d76f6e9422803acaadd8c1b046be8c647a
|
|
@ -0,0 +1 @@
|
|||
748ab129352d12d40e5d97dfb8a658ff2d62642e9f5cb1deb19ed871328f9d07
|
|
@ -0,0 +1 @@
|
|||
416ff5e57b2b13ccb55c6f1e88e6b0603dfc086a8a15be810752a9449ed4f3a1
|
|
@ -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. |
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 -->
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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).
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 | |
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue