Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue195
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue57
-rw-r--r--app/models/snippet.rb17
-rw-r--r--changelogs/unreleased/292017-fj-fix-bug-when-cloning-snippet-different-from-master.yml5
-rw-r--r--changelogs/unreleased/Migrate-Bootstrap-dropdown-to-GitLab-UI-GlDropdown-in-app-assets-javascri.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql53
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json115
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/ci/docker/using_docker_build.md95
-rw-r--r--doc/ci/docker/using_kaniko.md2
-rw-r--r--doc/user/packages/container_registry/index.md2
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md2
-rw-r--r--doc/user/project/merge_requests/code_quality.md2
-rw-r--r--doc/user/project/merge_requests/load_performance_testing.md2
-rw-r--r--lib/gitlab/git_access_snippet.rb5
-rw-r--r--locale/gitlab.pot14
-rw-r--r--spec/features/groups/board_spec.rb11
-rw-r--r--spec/frontend/boards/mock_data.js15
-rw-r--r--spec/frontend/boards/project_select_spec.js267
-rw-r--r--spec/frontend/vue_shared/components/pikaday_spec.js45
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb32
-rw-r--r--spec/models/snippet_spec.rb86
23 files changed, 742 insertions, 293 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8daccae3467..29703cac664 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -389,10 +389,12 @@ const Api = {
.get(url, {
params: { ...defaults, ...options },
})
- .then(({ data }) => callback(data))
+ .then(({ data }) => (callback ? callback(data) : data))
.catch(() => {
flash(__('Something went wrong while fetching projects'));
- callback();
+ if (callback) {
+ callback();
+ }
});
},
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 9c90938fc52..f152d708f2b 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,19 +1,37 @@
<script>
-import $ from 'jquery';
-import { escape } from 'lodash';
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import eventHub from '../eventhub';
+import { s__ } from '~/locale';
import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { ListType } from '../constants';
export default {
- name: 'BoardProjectSelect',
+ name: 'ProjectSelect',
+ i18n: {
+ headerTitle: s__(`BoardNewIssue|Projects`),
+ dropdownText: s__(`BoardNewIssue|Select a project`),
+ searchPlaceholder: s__(`BoardNewIssue|Search projects`),
+ emptySearchResult: s__(`BoardNewIssue|No matching results`),
+ },
+ defaultFetchOptions: {
+ with_issues_enabled: true,
+ with_shared: false,
+ include_subgroups: true,
+ order_by: 'similarity',
+ },
components: {
- GlIcon,
GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
},
props: {
list: {
@@ -24,97 +42,108 @@ export default {
inject: ['groupId'],
data() {
return {
- loading: true,
+ initialLoading: true,
+ isFetching: false,
+ projects: [],
selectedProject: {},
+ searchTerm: '',
};
},
computed: {
selectedProjectName() {
- return this.selectedProject.name || __('Select a project');
+ return this.selectedProject.name || this.$options.i18n.dropdownText;
+ },
+ fetchOptions() {
+ const additionalAttrs = {};
+ if (this.list.type && this.list.type !== ListType.backlog) {
+ additionalAttrs.min_access_level = featureAccessLevel.EVERYONE;
+ }
+
+ return {
+ ...this.$options.defaultFetchOptions,
+ ...additionalAttrs,
+ };
+ },
+ isFetchResultEmpty() {
+ return this.projects.length === 0;
},
},
- mounted() {
- initDeprecatedJQueryDropdown($(this.$refs.projectsDropdown), {
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name_with_namespace'],
- },
- clicked: ({ $el, e }) => {
- e.preventDefault();
- this.selectedProject = {
- id: $el.data('project-id'),
- name: $el.data('project-name'),
- path: $el.data('project-path'),
- };
- eventHub.$emit('setSelectedProject', this.selectedProject);
- },
- selectable: true,
- data: (term, callback) => {
- this.loading = true;
- const additionalAttrs = {};
+ watch: {
+ searchTerm() {
+ this.fetchProjects();
+ },
+ },
+ async mounted() {
+ await this.fetchProjects();
- if ((this.list.type || this.list.listType) !== ListType.backlog) {
- additionalAttrs.min_access_level = featureAccessLevel.EVERYONE;
- }
+ this.initialLoading = false;
+ },
+ methods: {
+ async fetchProjects() {
+ this.isFetching = true;
+ try {
+ const projects = await Api.groupProjects(this.groupId, this.searchTerm, this.fetchOptions);
- return Api.groupProjects(
- this.groupId,
- term,
- {
- with_issues_enabled: true,
- with_shared: false,
- include_subgroups: true,
- order_by: 'similarity',
- ...additionalAttrs,
- },
- projects => {
- this.loading = false;
- callback(projects);
- },
- );
- },
- renderRow(project) {
- return `
- <li>
- <a href='#' class='dropdown-menu-link'
- data-project-id="${project.id}"
- data-project-name="${project.name}"
- data-project-name-with-namespace="${project.name_with_namespace}"
- data-project-path="${project.path_with_namespace}"
- >
- ${escape(project.name_with_namespace)}
- </a>
- </li>
- `;
- },
- text: project => project.name_with_namespace,
- });
+ this.projects = projects.map(project => {
+ return {
+ id: project.id,
+ name: project.name,
+ namespacedName: project.name_with_namespace,
+ path: project.path_with_namespace,
+ };
+ });
+ } catch (err) {
+ /* Handled in Api.groupProjects */
+ } finally {
+ this.isFetching = false;
+ }
+ },
+ selectProject(projectId) {
+ this.selectedProject = this.projects.find(project => project.id === projectId);
+
+ /*
+ TODO Remove eventhub, use Vuex for BoardNewIssue and GraphQL for BoardNewIssueNew
+ https://gitlab.com/gitlab-org/gitlab/-/issues/276173
+ */
+ eventHub.$emit('setSelectedProject', this.selectedProject);
+ },
},
};
</script>
<template>
<div>
- <label class="label-bold gl-mt-3">{{ __('Project') }}</label>
- <div ref="projectsDropdown" class="dropdown dropdown-projects">
- <button
- class="dropdown-menu-toggle wide"
- type="button"
- data-toggle="dropdown"
- aria-expanded="false"
+ <label class="gl-font-weight-bold gl-mt-3" data-testid="header-label">{{
+ $options.i18n.headerTitle
+ }}</label>
+ <gl-dropdown
+ data-testid="project-select-dropdown"
+ :text="selectedProjectName"
+ :header-text="$options.i18n.headerTitle"
+ block
+ menu-class="gl-w-full!"
+ :loading="initialLoading"
+ >
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ debounce="250"
+ :placeholder="$options.i18n.searchPlaceholder"
+ />
+ <gl-dropdown-item
+ v-for="project in projects"
+ v-show="!isFetching"
+ :key="project.id"
+ :name="project.name"
+ @click="selectProject(project.id)"
>
- {{ selectedProjectName }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
- </button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
- <div class="dropdown-title">{{ __('Projects') }}</div>
- <div class="dropdown-input">
- <input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" />
- <gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><gl-loading-icon /></div>
- </div>
- </div>
+ {{ project.namespacedName }}
+ </gl-dropdown-item>
+ <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
+ <gl-loading-icon class="gl-mx-auto" />
+ </gl-dropdown-text>
+ <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
+ <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
+ </gl-dropdown-text>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 85481f3f7b4..3c0ac32e512 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -1,20 +1,13 @@
<script>
-import Pikaday from 'pikaday';
-import { GlIcon } from '@gitlab/ui';
-import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import { GlDatepicker } from '@gitlab/ui';
+import { pikadayToString } from '~/lib/utils/datetime_utility';
export default {
name: 'DatePicker',
components: {
- GlIcon,
+ GlDatepicker,
},
props: {
- label: {
- type: String,
- required: false,
- default: __('Date picker'),
- },
selectedDate: {
type: Date,
required: false,
@@ -31,32 +24,9 @@ export default {
default: null,
},
},
- mounted() {
- this.calendar = new Pikaday({
- field: this.$el.querySelector('.dropdown-menu-toggle'),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- container: this.$el,
- defaultDate: this.selectedDate,
- setDefaultDate: Boolean(this.selectedDate),
- minDate: this.minDate,
- maxDate: this.maxDate,
- parse: dateString => parsePikadayDate(dateString),
- toString: date => pikadayToString(date),
- onSelect: this.selected.bind(this),
- onClose: this.toggled.bind(this),
- firstDay: gon.first_day_of_week,
- });
-
- this.$el.append(this.calendar.el);
- this.calendar.show();
- },
- beforeDestroy() {
- this.calendar.destroy();
- },
methods: {
- selected(dateText) {
- this.$emit('newDateSelected', this.calendar.toString(dateText));
+ selected(date) {
+ this.$emit('newDateSelected', pikadayToString(date));
},
toggled() {
this.$emit('hidePicker');
@@ -66,12 +36,13 @@ export default {
</script>
<template>
- <div class="pikaday-container">
- <div class="dropdown open">
- <button type="button" class="dropdown-menu-toggle" data-toggle="dropdown" @click="toggled">
- <span class="dropdown-toggle-text"> {{ label }} </span>
- <gl-icon name="chevron-down" class="gl-absolute gl-right-3 gl-top-3 gl-text-gray-500" />
- </button>
- </div>
- </div>
+ <gl-datepicker
+ :value="selectedDate"
+ :min-date="minDate"
+ :max-date="maxDate"
+ start-opened
+ @close="toggled"
+ @click="toggled"
+ @input="selected"
+ />
</template>
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 817f9d014eb..23aab22fc09 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -20,6 +20,7 @@ class Snippet < ApplicationRecord
extend ::Gitlab::Utils::Override
MAX_FILE_COUNT = 10
+ MASTER_BRANCH = 'master'
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -311,13 +312,27 @@ class Snippet < ApplicationRecord
override :default_branch
def default_branch
- super || 'master'
+ super || MASTER_BRANCH
end
def repository_storage
snippet_repository&.shard_name || self.class.pick_repository_storage
end
+ # Repositories are created by default with the `master` branch.
+ # This method changes the `HEAD` file to point to the existing
+ # default branch in case it's not master.
+ def change_head_to_default_branch
+ return unless repository.exists?
+ return if default_branch == MASTER_BRANCH
+ # All snippets must have at least 1 file. Therefore, if
+ # `HEAD` is empty is because it's pointing to the wrong
+ # default branch
+ return unless repository.empty? || list_files('HEAD').empty?
+
+ repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}")
+ end
+
def create_repository
return if repository_exists? && snippet_repository
diff --git a/changelogs/unreleased/292017-fj-fix-bug-when-cloning-snippet-different-from-master.yml b/changelogs/unreleased/292017-fj-fix-bug-when-cloning-snippet-different-from-master.yml
new file mode 100644
index 00000000000..87a2ccb388d
--- /dev/null
+++ b/changelogs/unreleased/292017-fj-fix-bug-when-cloning-snippet-different-from-master.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug with snippets in HEAD when default branch is not master
+merge_request: 50366
+author:
+type: fixed
diff --git a/changelogs/unreleased/Migrate-Bootstrap-dropdown-to-GitLab-UI-GlDropdown-in-app-assets-javascri.yml b/changelogs/unreleased/Migrate-Bootstrap-dropdown-to-GitLab-UI-GlDropdown-in-app-assets-javascri.yml
new file mode 100644
index 00000000000..1d8a0b1be74
--- /dev/null
+++ b/changelogs/unreleased/Migrate-Bootstrap-dropdown-to-GitLab-UI-GlDropdown-in-app-assets-javascri.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate-Bootstrap-dropdown-to-GitLab-UI-GlDropdown-in-app/assets/javascripts/vue_shared/components/pikaday.vue
+merge_request: 41458
+author: nuwe1
+type: other
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 346652aa329..b1f43f38e51 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -3595,6 +3595,23 @@ type ComplianceFrameworkEdge {
node: ComplianceFramework
}
+input ComplianceFrameworkInput {
+ """
+ New color representation of the compliance framework in hex format. e.g. #FCA121.
+ """
+ color: String
+
+ """
+ New description for the compliance framework.
+ """
+ description: String
+
+ """
+ New name for the compliance framework.
+ """
+ name: String
+}
+
"""
Identifier of ComplianceManagement::Framework
"""
@@ -4386,24 +4403,14 @@ input CreateComplianceFrameworkInput {
clientMutationId: String
"""
- Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123.
- """
- color: String!
-
- """
- Description of the compliance framework.
- """
- description: String!
-
- """
- Name of the compliance framework.
+ Full path of the namespace to add the compliance framework to.
"""
- name: String!
+ namespacePath: ID!
"""
- Full path of the namespace to add the compliance framework to.
+ Parameters to update the compliance framework with.
"""
- namespacePath: ID!
+ params: ComplianceFrameworkInput!
}
"""
@@ -24295,24 +24302,14 @@ input UpdateComplianceFrameworkInput {
clientMutationId: String
"""
- New color representation of the compliance framework in hex format. e.g. #FCA121
- """
- color: String
-
- """
- New description for the compliance framework
- """
- description: String
-
- """
- The global ID of the compliance framework to update
+ The global ID of the compliance framework to update.
"""
id: ComplianceManagementFrameworkID!
"""
- New name for the compliance framework
+ Parameters to update the compliance framework with.
"""
- name: String
+ params: ComplianceFrameworkInput!
}
"""
@@ -24325,7 +24322,7 @@ type UpdateComplianceFrameworkPayload {
clientMutationId: String
"""
- The compliance framework after mutation
+ The compliance framework after mutation.
"""
complianceFramework: ComplianceFramework
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index cf63865ad7d..34d80e450f4 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -9835,6 +9835,47 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "ComplianceFrameworkInput",
+ "description": null,
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "name",
+ "description": "New name for the compliance framework.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "New description for the compliance framework.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "color",
+ "description": "New color representation of the compliance framework in hex format. e.g. #FCA121.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"description": "Identifier of ComplianceManagement::Framework",
@@ -11975,42 +12016,14 @@
"defaultValue": null
},
{
- "name": "name",
- "description": "Name of the compliance framework.",
+ "name": "params",
+ "description": "Parameters to update the compliance framework with.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- },
- {
- "name": "description",
- "description": "Description of the compliance framework.",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- },
- {
- "name": "color",
- "description": "Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123.",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
+ "kind": "INPUT_OBJECT",
+ "name": "ComplianceFrameworkInput",
"ofType": null
}
},
@@ -70788,7 +70801,7 @@
"inputFields": [
{
"name": "id",
- "description": "The global ID of the compliance framework to update",
+ "description": "The global ID of the compliance framework to update.",
"type": {
"kind": "NON_NULL",
"name": null,
@@ -70801,32 +70814,16 @@
"defaultValue": null
},
{
- "name": "name",
- "description": "New name for the compliance framework",
+ "name": "params",
+ "description": "Parameters to update the compliance framework with.",
"type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "description",
- "description": "New description for the compliance framework",
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "color",
- "description": "New color representation of the compliance framework in hex format. e.g. #FCA121",
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "ComplianceFrameworkInput",
+ "ofType": null
+ }
},
"defaultValue": null
},
@@ -70866,7 +70863,7 @@
},
{
"name": "complianceFramework",
- "description": "The compliance framework after mutation",
+ "description": "The compliance framework after mutation.",
"args": [
],
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index f05654c2c3d..439b3f8527a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3645,7 +3645,7 @@ Autogenerated return type of UpdateComplianceFramework.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `complianceFramework` | ComplianceFramework | The compliance framework after mutation |
+| `complianceFramework` | ComplianceFramework | The compliance framework after mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateContainerExpirationPolicyPayload
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 8e5ce2fb359..7b6466adbde 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -7,17 +7,17 @@ type: concepts, howto
# Building Docker images with GitLab CI/CD
-GitLab CI/CD allows you to use Docker Engine to build and test Docker-based projects.
+You can use GitLab CI/CD with Docker Engine to build and test Docker-based projects.
-One of the new trends in Continuous Integration/Deployment is to:
+For example, you might want to:
1. Create an application image.
1. Run tests against the created image.
1. Push image to a remote registry.
1. Deploy to a server from the pushed image.
-It's also useful when your application already has the `Dockerfile` that can be
-used to create and test an image:
+Or, if your application already has a `Dockerfile`, you can
+use it to create and test an image:
```shell
docker build -t my-image dockerfiles/
@@ -26,29 +26,37 @@ docker tag my-image my-registry:5000/my-image
docker push my-registry:5000/my-image
```
-This requires special configuration of GitLab Runner to enable `docker` support
-during jobs.
+To run Docker commands in your CI/CD jobs, you must configure
+GitLab Runner to enable `docker` support.
-## Runner Configuration
+## Enable Docker commands in your CI/CD jobs
-There are three methods to enable the use of `docker build` and `docker run`
-during jobs, each with their own tradeoffs.
+There are three ways to enable the use of `docker build` and `docker run`
+during jobs, each with their own tradeoffs. You can use:
-An alternative to using `docker build` is to [use kaniko](using_kaniko.md).
-This avoids having to execute a runner in privileged mode.
+- [The shell executor](#use-the-shell-executor)
+- [The Docker executor with the Docker image (Docker-in-Docker)](#use-the-docker-executor-with-the-docker-image-docker-in-docker)
+- [Docker socket binding](#use-docker-socket-binding)
-NOTE:
-To see how Docker and GitLab Runner are configured for shared runners on
-GitLab.com, see [GitLab.com shared
-runners](../../user/gitlab_com/index.md#shared-runners).
+If you don't want to execute a runner in privileged mode,
+but want to use `docker build`, you can also [use kaniko](using_kaniko.md).
+
+If you are using shared runners on GitLab.com, see
+[GitLab.com shared runners](../../user/gitlab_com/index.md#shared-runners)
+to learn more about how these runners are configured.
-### Use shell executor
+### Use the shell executor
-The simplest approach is to install GitLab Runner in `shell` execution mode.
-GitLab Runner then executes job scripts as the `gitlab-runner` user.
+One way to configure GitLab Runner for `docker` support is to use the
+`shell` executor.
-1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/#installation).
-1. During GitLab Runner installation select `shell` as method of executing job scripts or use command:
+After you register a runner and select the `shell` executor,
+your job scripts are executed as the `gitlab-runner` user.
+This user needs permission to run Docker commands.
+
+1. [Install](https://gitlab.com/gitlab-org/gitlab-runner/#installation) GitLab Runner.
+1. [Register](https://docs.gitlab.com/runner/register/) a runner.
+ Select the `shell` executor. For example:
```shell
sudo gitlab-runner register -n \
@@ -58,12 +66,10 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
--description "My Runner"
```
-1. Install Docker Engine on server.
-
- For more information how to install Docker Engine on different systems,
- check out the [Supported installations](https://docs.docker.com/engine/installation/).
+1. On the server where GitLab Runner is installed, install Docker Engine.
+ View a list of [supported platforms](https://docs.docker.com/engine/installation/).
-1. Add `gitlab-runner` user to `docker` group:
+1. Add the `gitlab-runner` user to the `docker` group:
```shell
sudo usermod -aG docker gitlab-runner
@@ -75,7 +81,7 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
sudo -u gitlab-runner -H docker info
```
- You can now verify that everything works by adding `docker info` to `.gitlab-ci.yml`:
+1. In GitLab, to verify that everything works, add `docker info` to `.gitlab-ci.yml`:
```yaml
before_script:
@@ -87,28 +93,30 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
- docker run my-docker-image /script/to/run/tests
```
-1. You can now use `docker` command (and **install** `docker-compose` if needed).
+You can now use `docker` commands (and install `docker-compose` if needed).
+
+When you add `gitlab-runner` to the `docker` group, you are effectively granting `gitlab-runner` full root permissions.
+Learn more about the [security of the `docker` group](https://blog.zopyx.com/on-docker-security-docker-group-considered-harmful/).
-By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
-For more information please read [On Docker security: `docker` group considered harmful](https://blog.zopyx.com/on-docker-security-docker-group-considered-harmful/).
+### Use the Docker executor with the Docker image (Docker-in-Docker)
-### Use Docker-in-Docker workflow with Docker executor
+Another way to configure GitLab Runner for `docker` support is to
+register a runner with the Docker executor and use the [Docker image](https://hub.docker.com/_/docker/)
+to run your job scripts. This configuration is referred to as "Docker-in-Docker."
-The second approach is to use the special Docker-in-Docker (dind)
-[Docker image](https://hub.docker.com/_/docker/) with all tools installed
-(`docker`) and run the job script in context of that
-image in privileged mode.
+The Docker image has all of the `docker` tools installed
+and can run the job script in context of the image in privileged mode.
-`docker-compose` is not part of Docker-in-Docker (dind). To use `docker-compose` in your
-CI builds, follow the `docker-compose`
+The `docker-compose` command is not available in this configuration by default.
+To use `docker-compose` in your job scripts, follow the `docker-compose`
[installation instructions](https://docs.docker.com/compose/install/).
WARNING:
-By enabling `--docker-privileged`, you are effectively disabling all of
+When you enable `--docker-privileged`, you are effectively disabling all of
the security mechanisms of containers and exposing your host to privilege
escalation which can lead to container breakout. For more information, check
out the official Docker documentation on
-[Runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities).
+[runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities).
Docker-in-Docker works well, and is the recommended configuration, but it is
not without its own challenges:
@@ -363,10 +371,11 @@ build:
- docker run my-docker-image /script/to/run/tests
```
-#### Use Docker socket binding
+### Use Docker socket binding
-The third approach is to bind-mount `/var/run/docker.sock` into the
-container so that Docker is available in the context of that image.
+Another way to configure GitLab Runner for `docker` support is to
+bind-mount `/var/run/docker.sock` into the
+container so that Docker is available in the context of the image.
NOTE:
If you bind the Docker socket and you are
@@ -854,13 +863,13 @@ After you've built a Docker image, you can push it up to the built-in
### `docker: Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?`
This is a common error when you are using
-[Docker in Docker](#use-docker-in-docker-workflow-with-docker-executor)
+[Docker in Docker](#use-the-docker-executor-with-the-docker-image-docker-in-docker)
v19.03 or higher.
This occurs because Docker starts on TLS automatically, so you need to do some setup.
If:
- This is the first time setting it up, carefully read
- [using Docker in Docker workflow](#use-docker-in-docker-workflow-with-docker-executor).
+ [using Docker in Docker workflow](#use-the-docker-executor-with-the-docker-image-docker-in-docker).
- You are upgrading from v18.09 or earlier, read our
[upgrade guide](https://about.gitlab.com/releases/2019/07/31/docker-in-docker-with-docker-19-dot-03/).
diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md
index 13d3c607f8a..89722dac419 100644
--- a/doc/ci/docker/using_kaniko.md
+++ b/doc/ci/docker/using_kaniko.md
@@ -14,7 +14,7 @@ container images from a Dockerfile, inside a container or Kubernetes cluster.
kaniko solves two problems with using the
[Docker-in-Docker
-build](using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor) method:
+build](using_docker_build.md#use-the-docker-executor-with-the-docker-image-docker-in-docker) method:
- Docker-in-Docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
to function, which is a significant security concern.
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 4e8d105adfa..63a4a6b3ed6 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -305,7 +305,7 @@ is set to `always`.
To use your own Docker images for Docker-in-Docker, follow these steps
in addition to the steps in the
-[Docker-in-Docker](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor) section:
+[Docker-in-Docker](../../../ci/docker/using_docker_build.md#use-the-docker-executor-with-the-docker-image-docker-in-docker) section:
1. Update the `image` and `service` to point to your registry.
1. Add a service [alias](../../../ci/yaml/README.md#servicesalias).
diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md
index 04114968c80..6fa2340c7a4 100644
--- a/doc/user/project/merge_requests/browser_performance_testing.md
+++ b/doc/user/project/merge_requests/browser_performance_testing.md
@@ -60,7 +60,7 @@ on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
using Docker-in-Docker.
1. First, set up GitLab Runner with a
- [Docker-in-Docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+ [Docker-in-Docker build](../../../ci/docker/using_docker_build.md#use-the-docker-executor-with-the-docker-image-docker-in-docker).
1. Configure the default Browser Performance Testing CI job as follows in your `.gitlab-ci.yml` file:
```yaml
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 5a98338a81b..426973d9fd7 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -74,7 +74,7 @@ GitLab 11.4 or earlier, you can view the deprecated job definitions in the
First, you need GitLab Runner configured:
-- For the [Docker-in-Docker workflow](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+- For the [Docker-in-Docker workflow](../../../ci/docker/using_docker_build.md#use-the-docker-executor-with-the-docker-image-docker-in-docker).
- With enough disk space to handle generated Code Quality files. For example on the [GitLab project](https://gitlab.com/gitlab-org/gitlab) the files are approximately 7 GB.
Once you set up GitLab Runner, include the Code Quality template in your CI configuration:
diff --git a/doc/user/project/merge_requests/load_performance_testing.md b/doc/user/project/merge_requests/load_performance_testing.md
index 82b5d67ba2b..9154897d42d 100644
--- a/doc/user/project/merge_requests/load_performance_testing.md
+++ b/doc/user/project/merge_requests/load_performance_testing.md
@@ -103,7 +103,7 @@ job.
An example configuration workflow:
1. Set up GitLab Runner to run Docker containers, like the
- [Docker-in-Docker workflow](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+ [Docker-in-Docker workflow](../../../ci/docker/using_docker_build.md#use-the-docker-executor-with-the-docker-image-docker-in-docker).
1. Configure the default Load Performance Testing CI job in your `.gitlab-ci.yml` file.
You need to include the template and configure it with variables:
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index 854bf6e9c9e..482881daca6 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -30,7 +30,10 @@ module Gitlab
def check(cmd, changes)
check_snippet_accessibility!
- super
+ super.tap do |_|
+ # Ensure HEAD points to the default branch in case it is not master
+ snippet.change_head_to_default_branch
+ end
end
override :download_ability
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f6b4e5ceb3e..48eb4cc2812 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4190,7 +4190,7 @@ msgstr ""
msgid "AutoRemediation|%{mrsCount} ready for review"
msgstr ""
-msgid "AutoRemediation|: Auto-fix"
+msgid "AutoRemediation|Auto-fix"
msgstr ""
msgid "AutoRemediation|Auto-fix solutions"
@@ -4534,6 +4534,18 @@ msgstr ""
msgid "Board scope affects which issues are displayed for anyone who visits this board"
msgstr ""
+msgid "BoardNewIssue|No matching results"
+msgstr ""
+
+msgid "BoardNewIssue|Projects"
+msgstr ""
+
+msgid "BoardNewIssue|Search projects"
+msgstr ""
+
+msgid "BoardNewIssue|Select a project"
+msgstr ""
+
msgid "Boards"
msgstr ""
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index b25aa26d906..aab3f5e68d5 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -20,14 +20,19 @@ RSpec.describe 'Group Boards' do
page.within(find('.board', match: :first)) do
issue_title = 'New Issue'
find(:css, '.issue-count-badge-add-button').click
+
+ wait_for_requests
+
expect(find('.board-new-issue-form')).to be_visible
fill_in 'issue_title', with: issue_title
- find('.dropdown-menu-toggle').click
- wait_for_requests
+ page.within("[data-testid='project-select-dropdown']") do
+ find('button.gl-dropdown-toggle').click
+
+ find('.gl-new-dropdown-item button').click
+ end
- click_link(project.name)
click_button 'Submit issue'
expect(page).to have_content(issue_title)
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index ea6c52c6830..1319640b93c 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -350,3 +350,18 @@ export const issues = {
[mockIssue3.id]: mockIssue3,
[mockIssue4.id]: mockIssue4,
};
+
+export const mockRawGroupProjects = [
+ {
+ id: 0,
+ name: 'Example Project',
+ name_with_namespace: 'Awesome Group / Example Project',
+ path_with_namespace: 'awesome-group/example-project',
+ },
+ {
+ id: 1,
+ name: 'Foobar Project',
+ name_with_namespace: 'Awesome Group / Foobar Project',
+ path_with_namespace: 'awesome-group/foobar-project',
+ },
+];
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
new file mode 100644
index 00000000000..e187828702e
--- /dev/null
+++ b/spec/frontend/boards/project_select_spec.js
@@ -0,0 +1,267 @@
+import { mount } from '@vue/test-utils';
+import axios from 'axios';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import httpStatus from '~/lib/utils/http_status';
+import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import { ListType } from '~/boards/constants';
+import eventHub from '~/boards/eventhub';
+import { deprecatedCreateFlash as flash } from '~/flash';
+
+import ProjectSelect from '~/boards/components/project_select.vue';
+
+import { listObj, mockRawGroupProjects } from './mock_data';
+
+jest.mock('~/boards/eventhub');
+jest.mock('~/flash');
+
+const dummyGon = {
+ api_version: 'v4',
+ relative_url_root: '/gitlab',
+};
+
+const mockGroupId = 1;
+const mockProjectsList1 = mockRawGroupProjects.slice(0, 1);
+const mockProjectsList2 = mockRawGroupProjects.slice(1);
+const mockDefaultFetchOptions = {
+ with_issues_enabled: true,
+ with_shared: false,
+ include_subgroups: true,
+ order_by: 'similarity',
+};
+
+const itemsPerPage = 20;
+
+describe('ProjectSelect component', () => {
+ let wrapper;
+ let axiosMock;
+
+ const findLabel = () => wrapper.find("[data-testid='header-label']");
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdownLoadingIcon = () =>
+ findGlDropdown()
+ .find('button:first-child')
+ .find(GlLoadingIcon);
+ const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
+ const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']");
+ const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']");
+
+ const mockGetRequest = (data = [], statusCode = httpStatus.OK) => {
+ axiosMock
+ .onGet(`/gitlab/api/v4/groups/${mockGroupId}/projects.json`)
+ .replyOnce(statusCode, data);
+ };
+
+ const searchForProject = async (keyword, waitForAll = true) => {
+ findGlSearchBoxByType().vm.$emit('input', keyword);
+
+ if (waitForAll) {
+ await axios.waitForAll();
+ }
+ };
+
+ const createWrapper = async ({ list = listObj } = {}, waitForAll = true) => {
+ wrapper = mount(ProjectSelect, {
+ propsData: {
+ list,
+ },
+ provide: {
+ groupId: 1,
+ },
+ });
+
+ if (waitForAll) {
+ await axios.waitForAll();
+ }
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ window.gon = dummyGon;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ axiosMock.restore();
+ jest.clearAllMocks();
+ });
+
+ it('displays a header title', async () => {
+ createWrapper({});
+
+ expect(findLabel().text()).toBe('Projects');
+ });
+
+ it('renders a default dropdown text', async () => {
+ createWrapper({});
+
+ expect(findGlDropdown().exists()).toBe(true);
+ expect(findGlDropdown().text()).toContain('Select a project');
+ });
+
+ describe('when mounted', () => {
+ it('displays a loading icon while projects are being fetched', async () => {
+ mockGetRequest([]);
+
+ createWrapper({}, false);
+
+ expect(findGlDropdownLoadingIcon().exists()).toBe(true);
+
+ await axios.waitForAll();
+
+ expect(axiosMock.history.get[0].params).toMatchObject({ search: '' });
+ expect(axiosMock.history.get[0].url).toBe(
+ `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
+ );
+
+ expect(findGlDropdownLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when dropdown menu is open', () => {
+ describe('by default', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList1);
+
+ await createWrapper();
+ });
+
+ it('shows GlSearchBoxByType with default attributes', () => {
+ expect(findGlSearchBoxByType().exists()).toBe(true);
+ expect(findGlSearchBoxByType().vm.$attrs).toMatchObject({
+ placeholder: 'Search projects',
+ debounce: '250',
+ });
+ });
+
+ it("displays the fetched project's name", () => {
+ expect(findFirstGlDropdownItem().exists()).toBe(true);
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name);
+ });
+
+ it("doesn't render loading icon in the menu", () => {
+ expect(findInMenuLoadingIcon().isVisible()).toBe(false);
+ });
+
+ it('renders empty search result message', async () => {
+ await createWrapper();
+
+ expect(findEmptySearchMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList1);
+
+ await createWrapper();
+
+ await findFirstGlDropdownItem()
+ .find('button')
+ .trigger('click');
+ });
+
+ it('emits setSelectedProject with correct project metadata', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('setSelectedProject', {
+ id: mockProjectsList1[0].id,
+ path: mockProjectsList1[0].path_with_namespace,
+ name: mockProjectsList1[0].name,
+ namespacedName: mockProjectsList1[0].name_with_namespace,
+ });
+ });
+
+ it('renders the name of the selected project', () => {
+ expect(
+ findGlDropdown()
+ .find('.gl-new-dropdown-button-text')
+ .text(),
+ ).toBe(mockProjectsList1[0].name);
+ });
+ });
+
+ describe('when user searches for a project', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList1);
+
+ await createWrapper();
+ });
+
+ it('calls API with correct parameters with default fetch options', async () => {
+ await searchForProject('foobar');
+
+ const expectedApiParams = {
+ search: 'foobar',
+ per_page: itemsPerPage,
+ ...mockDefaultFetchOptions,
+ };
+
+ expect(axiosMock.history.get[1].params).toMatchObject(expectedApiParams);
+ expect(axiosMock.history.get[1].url).toBe(
+ `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
+ );
+ });
+
+ describe("when list type is defined and isn't backlog", () => {
+ it('calls API with an additional fetch option (min_access_level)', async () => {
+ axiosMock.reset();
+
+ await createWrapper({ list: { ...listObj, type: ListType.label } });
+
+ await searchForProject('foobar');
+
+ const expectedApiParams = {
+ search: 'foobar',
+ per_page: itemsPerPage,
+ ...mockDefaultFetchOptions,
+ min_access_level: featureAccessLevel.EVERYONE,
+ };
+
+ expect(axiosMock.history.get[1].params).toMatchObject(expectedApiParams);
+ expect(axiosMock.history.get[1].url).toBe(
+ `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
+ );
+ });
+ });
+
+ it('displays and hides gl-loading-icon while and after fetching data', async () => {
+ await searchForProject('some keyword', false);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findInMenuLoadingIcon().isVisible()).toBe(true);
+
+ await axios.waitForAll();
+
+ expect(findInMenuLoadingIcon().isVisible()).toBe(false);
+ });
+
+ it('flashes an error message when fetching fails', async () => {
+ mockGetRequest([], httpStatus.INTERNAL_SERVER_ERROR);
+
+ await searchForProject('foobar');
+
+ expect(flash).toHaveBeenCalledTimes(1);
+ expect(flash).toHaveBeenCalledWith('Something went wrong while fetching projects');
+ });
+
+ describe('with non-empty search result', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList2);
+
+ await searchForProject('foobar');
+ });
+
+ it('displays the retrieved list of projects', async () => {
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList2[0].name);
+ });
+
+ it('does not render empty search result message', async () => {
+ expect(findEmptySearchMessage().exists()).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js
index 639b4828a09..1c6876c282c 100644
--- a/spec/frontend/vue_shared/components/pikaday_spec.js
+++ b/spec/frontend/vue_shared/components/pikaday_spec.js
@@ -1,42 +1,41 @@
import { shallowMount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
import datePicker from '~/vue_shared/components/pikaday.vue';
describe('datePicker', () => {
let wrapper;
- beforeEach(() => {
+
+ const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(datePicker, {
- propsData: {
- label: 'label',
- },
- attachToDocument: true,
+ propsData,
});
- });
+ };
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
+ it('should emit newDateSelected when GlDatePicker emits the input event', () => {
+ const minDate = new Date();
+ const maxDate = new Date();
+ const selectedDate = new Date();
+ const theDate = selectedDate.toISOString().slice(0, 10);
- it('should render label text', () => {
- expect(
- wrapper
- .find('.dropdown-toggle-text')
- .text()
- .trim(),
- ).toEqual('label');
- });
+ buildWrapper({ minDate, maxDate, selectedDate });
- it('should show calendar', () => {
- expect(wrapper.find('.pika-single').element).toBeDefined();
+ expect(wrapper.find(GlDatepicker).props()).toMatchObject({
+ minDate,
+ maxDate,
+ value: selectedDate,
+ });
+ wrapper.find(GlDatepicker).vm.$emit('input', selectedDate);
+ expect(wrapper.emitted('newDateSelected')[0][0]).toBe(theDate);
});
+ it('should emit the hidePicker event when GlDatePicker emits the close event', () => {
+ buildWrapper();
- it('should emit hidePicker event when dropdown is clicked', () => {
- // Removing the bootstrap data-toggle property,
- // because it interfers with our click event
- delete wrapper.find('.dropdown-menu-toggle').element.dataset.toggle;
-
- wrapper.find('.dropdown-menu-toggle').trigger('click');
+ wrapper.find(GlDatepicker).vm.$emit('close');
- expect(wrapper.emitted('hidePicker')).toEqual([[]]);
+ expect(wrapper.emitted('hidePicker')).toHaveLength(1);
});
});
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index 3deccc911e5..4cee7a86411 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -390,6 +390,38 @@ RSpec.describe Gitlab::GitAccessSnippet do
end
end
+ describe 'HEAD realignment' do
+ let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project) }
+
+ shared_examples 'HEAD is updated to the snippet default branch' do
+ let(:actor) { snippet.author }
+
+ specify do
+ expect(snippet).to receive(:change_head_to_default_branch).and_call_original
+
+ subject
+ end
+
+ context 'when an error is raised' do
+ let(:actor) { nil }
+
+ it 'does not realign HEAD' do
+ expect(snippet).not_to receive(:change_head_to_default_branch).and_call_original
+
+ expect { subject }.to raise_error(described_class::ForbiddenError)
+ end
+ end
+ end
+
+ it_behaves_like 'HEAD is updated to the snippet default branch' do
+ subject { push_access_check }
+ end
+
+ it_behaves_like 'HEAD is updated to the snippet default branch' do
+ subject { pull_access_check }
+ end
+ end
+
private
def raise_snippet_not_found
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index f87259ea048..68d183d5d55 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -796,4 +796,90 @@ RSpec.describe Snippet do
it_behaves_like 'can move repository storage' do
let_it_be(:container) { create(:snippet, :repository) }
end
+
+ describe '#change_head_to_default_branch' do
+ let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{snippet.disk_path}.git", 'HEAD') }
+
+ subject { snippet.change_head_to_default_branch }
+
+ context 'when repository does not exist' do
+ let(:snippet) { create(:snippet) }
+
+ it 'does nothing' do
+ expect(snippet.repository_exists?).to eq false
+ expect(snippet.repository.raw_repository).not_to receive(:write_ref)
+
+ subject
+ end
+ end
+
+ context 'when repository is empty' do
+ let(:snippet) { create(:snippet, :empty_repo) }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch)
+ end
+
+ context 'when default branch in settings is "master"' do
+ let(:default_branch) { 'master' }
+
+ it 'does nothing' do
+ expect(File.read(head_path).squish).to eq 'ref: refs/heads/master'
+
+ expect(snippet.repository.raw_repository).not_to receive(:write_ref)
+
+ subject
+ end
+ end
+
+ context 'when default branch in settings is different from "master"' do
+ let(:default_branch) { 'main' }
+
+ it 'changes the HEAD reference to the default branch' do
+ expect(File.read(head_path).squish).to eq 'ref: refs/heads/master'
+
+ subject
+
+ expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
+ end
+ end
+ end
+
+ context 'when repository is not empty' do
+ let(:snippet) { create(:snippet, :empty_repo) }
+
+ before do
+ populate_snippet_repo
+ end
+
+ context 'when HEAD branch is empty' do
+ it 'changes HEAD to default branch' do
+ File.write(head_path, 'ref: refs/heads/non_existen_branch')
+ expect(File.read(head_path).squish).to eq 'ref: refs/heads/non_existen_branch'
+
+ subject
+
+ expect(File.read(head_path).squish).to eq 'ref: refs/heads/main'
+ expect(snippet.list_files('HEAD')).not_to be_empty
+ end
+ end
+
+ context 'when HEAD branch is not empty' do
+ it 'does nothing' do
+ File.write(head_path, 'ref: refs/heads/main')
+
+ expect(snippet.repository.raw_repository).not_to receive(:write_ref)
+
+ subject
+ end
+ end
+
+ def populate_snippet_repo
+ allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return('main')
+
+ data = [{ file_path: 'new_file_test', content: 'bar' }]
+ snippet.snippet_repository.multi_files_action(snippet.author, data, branch_name: 'main', message: 'foo')
+ end
+ end
+ end
end