Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-26 15:08:58 +00:00
parent 0121231095
commit ff89c3c372
55 changed files with 1674 additions and 194 deletions

View File

@ -2205,7 +2205,7 @@ Gitlab/NamespacedClass:
- 'app/validators/system_hook_url_validator.rb'
- 'app/validators/top_level_group_validator.rb'
- 'app/validators/untrusted_regexp_validator.rb'
- 'app/validators/variable_duplicates_validator.rb'
- 'app/validators/nested_attributes_duplicates_validator.rb'
- 'app/validators/x509_certificate_credentials_validator.rb'
- 'app/validators/zoom_url_validator.rb'
- 'app/workers/admin_email_worker.rb'

View File

@ -1 +1 @@
99f78e4d93d8c9ec23ef710ffde0fb4b75d786bb
627c53e3e51f73c3d19df2b49b956c02ba200e78

View File

@ -26,8 +26,8 @@ export default {
</script>
<template>
<div class="info-well">
<div class="well-segment admin-well admin-well-statistics">
<div class="gl-card">
<div class="gl-card-body">
<h4>{{ __('Statistics') }}</h4>
<gl-loading-icon v-if="isLoading" size="md" class="my-3" />
<template v-else>

View File

@ -1,10 +1,3 @@
.info-well {
.admin-well-statistics,
.admin-well-features {
padding-bottom: 46px;
}
}
.usage-data {
max-height: 400px;
}

View File

@ -319,9 +319,7 @@ class GroupsController < Groups::ApplicationController
private
def successful_creation_hooks
track_experiment_event(:onboarding_issues, 'created_namespace')
end
def successful_creation_hooks; end
def groups
if @group.supports_events?

View File

@ -14,7 +14,6 @@ module Registrations
if current_user.save
hide_advanced_issues
record_experiment_user(:default_to_issues_board)
if experiment_enabled?(:default_to_issues_board) && learn_gitlab.available?
redirect_to namespace_project_board_path(params[:namespace_path], learn_gitlab.project, learn_gitlab.board)

View File

@ -39,9 +39,6 @@ module Registrations
def process_gitlab_com_tracking
return false unless ::Gitlab.com?
return false unless show_onboarding_issues_experiment?
track_experiment_event(:onboarding_issues, 'signed_up')
record_experiment_user(:onboarding_issues)
end
def update_params

View File

@ -21,7 +21,7 @@ module Ci
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :description, presence: true
validates :variables, variable_duplicates: true
validates :variables, nested_attributes_duplicates: true
strip_attributes :cron

View File

@ -84,7 +84,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed
validates :variables, variable_duplicates: true
validates :variables, nested_attributes_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }

View File

@ -456,7 +456,7 @@ class Project < ApplicationRecord
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: { scope: :environment_scope }
validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }

View File

@ -11,5 +11,5 @@ class ProjectPagesMetadatum < ApplicationRecord
scope :deployed, -> { where(deployed: true) }
scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) }
scope :with_project_route_and_deployment, -> { preload(project: [:namespace, :route, pages_metadatum: :pages_deployment]) }
scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end

View File

@ -24,6 +24,7 @@ class Release < ApplicationRecord
validates :project, :tag, presence: true
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
scope :sorted, -> { order(released_at: :desc) }
scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }

View File

@ -1059,6 +1059,10 @@ class Repository
blob_data_at(sha, '.lfsconfig')
end
def changelog_config(ref = 'HEAD')
blob_data_at(ref, Gitlab::Changelog::Config::FILE_PATH)
end
def fetch_ref(source_repository, source_ref:, target_ref:)
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end

View File

@ -32,8 +32,10 @@ module Pages
def start_migration_threads
Array.new(@migration_threads) do
Thread.new do
while batch = @queue.pop
process_batch(batch)
Rails.application.executor.wrap do
while batch = @queue.pop
process_batch(batch)
end
end
end
end

View File

@ -2,8 +2,9 @@
module Suggestions
class ApplyService < ::BaseService
def initialize(current_user, *suggestions)
def initialize(current_user, *suggestions, message: nil)
@current_user = current_user
@message = message
@suggestion_set = Gitlab::Suggestions::SuggestionSet.new(suggestions)
end
@ -47,7 +48,7 @@ module Suggestions
end
def commit_message
Gitlab::Suggestions::CommitMessage.new(current_user, suggestion_set).message
Gitlab::Suggestions::CommitMessage.new(current_user, suggestion_set, @message).message
end
end
end

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true
# VariableDuplicatesValidator
# NestedAttributesDuplicates
#
# This validator is designed for especially the following condition
# - Use `accepts_nested_attributes_for :xxx` in a parent model
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
class NestedAttributesDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if record.errors.include?(:"#{attribute}.key")
return if child_attributes.any? { |child_attribute| record.errors.include?(:"#{attribute}.#{child_attribute}") }
if options[:scope]
scoped = value.group_by do |variable|
@ -23,12 +23,18 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator
# rubocop: disable CodeReuse/ActiveRecord
def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
error_message = +"have duplicate values (#{duplicates.join(", ")})"
error_message << " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message)
child_attributes.each do |child_attribute|
duplicates = values.reject(&:marked_for_destruction?).group_by(&:"#{child_attribute}").select { |_, v| v.many? }.map(&:first)
if duplicates.any?
error_message = +"have duplicate values (#{duplicates.join(", ")})"
error_message << " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def child_attributes
options[:child_attributes] || %i[key]
end
end

View File

@ -15,49 +15,63 @@
= render_if_exists 'admin/licenses/breakdown'
.admin-dashboard.gl-mt-3
.h3.gl-mb-5.gl-mt-0= _('Instance overview')
.row
.col-sm-4
.info-well.dark-well.flex-fill
.well-segment.well-centered
= link_to admin_projects_path do
%h3.text-center
= s_('AdminArea|Projects: %{number_of_projects}') % { number_of_projects: approximate_count_with_delimiters(@counts, Project) }
%hr
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4
.info-well.dark-well
.well-segment.well-centered.gl-text-center
= link_to admin_users_path do
%h3.gl-display-inline-block.gl-mb-0
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%span.gl-outline-0.gl-ml-2{ href: "#", tabindex: "0", data: { container: "body",
toggle: "popover",
placement: "top",
html: "true",
trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700 gl-mb-1')
%hr
.btn-group.d-flex{ role: 'group' }
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn gl-button btn-info gl-w-full'
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
= link_to admin_groups_path do
%h3.text-center
= s_('AdminArea|Groups: %{number_of_groups}') % { number_of_groups: approximate_count_with_delimiters(@counts, Group) }
%hr
= link_to s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-success gl-w-full"
.col-md-4.gl-mb-6
.gl-card
.gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
%span
.d-flex.align-items-center
= sprite_icon('project', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Project)
.gl-mt-3.text-uppercase= s_('AdminArea|Projects')
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-default")
.gl-card-footer.gl-bg-transparent
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest projects'), admin_projects_path)
= sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
.gl-card
.gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
%span
.d-flex.align-items-center
= sprite_icon('users', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, User)
%span.gl-outline-0.gl-ml-3{ tabindex: "0", data: { container: "body",
toggle: "popover",
placement: "top",
html: "true",
trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700')
.gl-mt-3.text-uppercase
= s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
= link_to(s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-default")
.gl-card-footer.gl-bg-transparent
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest users'), admin_users_path)
= sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
.gl-card
.gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
%span
.d-flex.align-items-center
= sprite_icon('group', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Group)
.gl-mt-3.text-uppercase= s_('AdminArea|Projects')
= link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default")
.gl-card-footer.gl-bg-transparent
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest groups'), admin_groups_path)
= sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.row
.col-md-4
.col-md-4.gl-mb-6
#js-admin-statistics-container
.col-md-4
.info-well
.well-segment.admin-well.admin-well-features
.col-md-4.gl-mb-6
.gl-card
.gl-card-body
%h4= s_('AdminArea|Features')
= feature_entry(_('Sign up'),
href: general_admin_application_settings_path(anchor: 'js-signup-settings'),
@ -94,9 +108,9 @@
= feature_entry(_('Shared Runners'),
href: admin_runners_path,
enabled: Gitlab.config.gitlab_ci.shared_runners_enabled)
.col-md-4
.info-well
.well-segment.admin-well
.col-md-4.gl-mb-6
.gl-card
.gl-card-body
%h4
= s_('AdminArea|Components')
- if Gitlab::CurrentSettings.version_check_enabled
@ -146,18 +160,18 @@
%p
= link_to _("Gitaly Servers"), admin_gitaly_servers_path
.row
.col-md-4
.info-well
.well-segment.admin-well
.col-md-4.gl-mb-6
.gl-card
.gl-card-body
%h4= s_('AdminArea|Latest projects')
- @projects.each do |project|
%p
= link_to project.full_name, admin_project_path(project), class: 'str-truncated-60'
%span.light.float-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
.info-well
.well-segment.admin-well
.col-md-4.gl-mb-6
.gl-card
.gl-card-body
%h4= s_('AdminArea|Latest users')
- @users.each do |user|
%p
@ -165,9 +179,9 @@
= user.name
%span.light.float-right
#{time_ago_with_tooltip(user.created_at)}
.col-md-4
.info-well
.well-segment.admin-well
.col-md-4.gl-mb-6
.gl-card
.gl-card-body
%h4= s_('AdminArea|Latest groups')
- @groups.each do |group|
%p

View File

@ -28,7 +28,7 @@
- if current_user_menu?(:start_trial)
%li
%a.trial-link{ href: trials_link_url }
= s_("CurrentUser|Start a Gold trial")
= s_("CurrentUser|Start an Ultimate trial")
= emoji_icon('rocket')
- if current_user_menu?(:settings)
%li

View File

@ -0,0 +1,5 @@
---
title: Add a commit message parameter for the suggestion endpoints
merge_request: 51245
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Improve duplication validation on Release Links
merge_request: 51951
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Admin dashboard basic stats redesign
merge_request: 52176
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,47 @@
---
stage: Enablement
group: Memory
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
---
# Changing application settings cache expiry interval **(CORE ONLY)**
Application settings are cached for 60 seconds by default which should work
for most installations. A higher value would mean a greater delay between
changing an application setting and noticing that change come into effect.
A value of `0` would result in the `application_settings` table being
loaded for every request causing extra load on Redis and/or PostgreSQL.
It is therefore recommended to keep the value above zero.
## Change the application settings cache expiry
To change the expiry value:
**For Omnibus installations**
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['application_settings_cache_seconds'] = 60
```
1. Save the file, and reconfigure and restart GitLab for the changes to take effect:
```shell
gitlab-ctl reconfigure
gitlab-ctl restart
```
---
**For installations from source**
1. Edit `config/gitlab.yml`:
```yaml
gitlab:
application_settings_cache_seconds: 60
```
1. Save the file and [restart](restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect.

View File

@ -80,6 +80,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
emails with S/MIME.
- [Enabling and disabling features flags](feature_flags.md): how to enable and
disable GitLab features deployed behind feature flags.
- [Application settings cache expiry interval](application_settings_cache.md)
#### Customizing GitLab appearance

View File

@ -112,7 +112,24 @@ _The uploads are stored by default in
```
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate:all` Rake task](raketasks/uploads/migrate.md).
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` Rake task](raketasks/uploads/migrate.md).
1. Optional: Verify all files migrated properly.
From [PostgreSQL console](https://docs.gitlab.com/omnibus/settings/database.html#connecting-to-the-bundled-postgresql-database)
(`sudo gitlab-psql -d gitlabhq_production`) verify `objectstg` below (where `store=2`) has count of all artifacts:
```shell
gitlabhq_production=# SELECT count(*) AS total, sum(case when store = '1' then 1 else 0 end) AS filesystem, sum(case when store = '2' then 1 else 0 end) AS objectstg FROM uploads;
total | filesystem | objectstg
------+------------+-----------
2409 | 0 | 2409
```
Verify no files on disk in `artifacts` folder:
```shell
sudo find /var/opt/gitlab/gitlab-rails/uploads -type f | grep -v tmp | wc -l
```
**In installations from source:**
@ -136,6 +153,22 @@ _The uploads are stored by default in
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate:all` Rake task](raketasks/uploads/migrate.md).
1. Optional: Verify all files migrated properly.
From PostgreSQL console (`sudo -u git -H psql -d gitlabhq_production`) verify `objectstg` below (where `file_store=2`) has count of all artifacts:
```shell
gitlabhq_production=# SELECT count(*) AS total, sum(case when store = '1' then 1 else 0 end) AS filesystem, sum(case when store = '2' then 1 else 0 end) AS objectstg FROM uploads;
total | filesystem | objectstg
------+------------+-----------
2409 | 0 | 2409
```
Verify no files on disk in `artifacts` folder:
```shell
sudo find /var/opt/gitlab/gitlab-rails/uploads -type f | grep -v tmp | wc -l
```
#### OpenStack example
@ -162,6 +195,23 @@ _The uploads are stored by default in
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate:all` Rake task](raketasks/uploads/migrate.md).
1. Optional: Verify all files migrated properly.
From [PostgreSQL console](https://docs.gitlab.com/omnibus/settings/database.html#connecting-to-the-bundled-postgresql-database)
(`sudo gitlab-psql -d gitlabhq_production`) verify `objectstg` below (where `store=2`) has count of all artifacts:
```shell
gitlabhq_production=# SELECT count(*) AS total, sum(case when store = '1' then 1 else 0 end) AS filesystem, sum(case when store = '2' then 1 else 0 end) AS objectstg FROM uploads;
total | filesystem | objectstg
------+------------+-----------
2409 | 0 | 2409
```
Verify no files on disk in `artifacts` folder:
```shell
sudo find /var/opt/gitlab/gitlab-rails/uploads -type f | grep -v tmp | wc -l
```
---
@ -193,3 +243,19 @@ _The uploads are stored by default in
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate:all` Rake task](raketasks/uploads/migrate.md).
1. Optional: Verify all files migrated properly.
From PostgreSQL console (`sudo -u git -H psql -d gitlabhq_production`) verify `objectstg` below (where `file_store=2`) has count of all artifacts:
```shell
gitlabhq_production=# SELECT count(*) AS total, sum(case when store = '1' then 1 else 0 end) AS filesystem, sum(case when store = '2' then 1 else 0 end) AS objectstg FROM uploads;
total | filesystem | objectstg
------+------------+-----------
2409 | 0 | 2409
```
Verify no files on disk in `artifacts` folder:
```shell
sudo find /var/opt/gitlab/gitlab-rails/uploads -type f | grep -v tmp | wc -l
```

View File

@ -21,6 +21,7 @@ PUT /suggestions/:id/apply
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a suggestion |
| `commit_message` | string | no | A custom commit message to use instead of the default generated message or the project's default message |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/suggestions/5/apply"

View File

@ -543,6 +543,88 @@ test += " world"
When adding new Ruby files, please check that you can add the above header,
as omitting it may lead to style check failures.
## Banzai pipelines and filters
When writing or updating [Banzai filters and pipelines](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/banzai),
it can be difficult to understand what the performance of the filter is, and what effect it might
have on the overall pipeline performance.
To perform benchmarks run:
```shell
bin/rake benchmark:banzai
```
This command generates output like this:
```plaintext
--> Benchmarking Full, Wiki, and Plain pipelines
Calculating -------------------------------------
Full pipeline 1.000 i/100ms
Wiki pipeline 1.000 i/100ms
Plain pipeline 1.000 i/100ms
-------------------------------------------------
Full pipeline 3.357 (±29.8%) i/s - 31.000
Wiki pipeline 2.893 (±34.6%) i/s - 25.000 in 10.677014s
Plain pipeline 15.447 (±32.4%) i/s - 119.000
Comparison:
Plain pipeline: 15.4 i/s
Full pipeline: 3.4 i/s - 4.60x slower
Wiki pipeline: 2.9 i/s - 5.34x slower
.
--> Benchmarking FullPipeline filters
Calculating -------------------------------------
Markdown 24.000 i/100ms
Plantuml 8.000 i/100ms
SpacedLink 22.000 i/100ms
...
TaskList 49.000 i/100ms
InlineDiff 9.000 i/100ms
SetDirection 369.000 i/100ms
-------------------------------------------------
Markdown 237.796 (±16.4%) i/s - 2.304k
Plantuml 80.415 (±36.1%) i/s - 520.000
SpacedLink 168.188 (±10.1%) i/s - 1.672k
...
TaskList 101.145 (± 6.9%) i/s - 1.029k
InlineDiff 52.925 (±15.1%) i/s - 522.000
SetDirection 3.728k (±17.2%) i/s - 34.317k in 10.617882s
Comparison:
Suggestion: 739616.9 i/s
Kroki: 306449.0 i/s - 2.41x slower
InlineGrafanaMetrics: 156535.6 i/s - 4.72x slower
SetDirection: 3728.3 i/s - 198.38x slower
...
UserReference: 2.1 i/s - 360365.80x slower
ExternalLink: 1.6 i/s - 470400.67x slower
ProjectReference: 0.7 i/s - 1128756.09x slower
.
--> Benchmarking PlainMarkdownPipeline filters
Calculating -------------------------------------
Markdown 19.000 i/100ms
-------------------------------------------------
Markdown 241.476 (±15.3%) i/s - 2.356k
```
This can give you an idea how various filters perform, and which ones might be performing the slowest.
The test data has a lot to do with how well a filter performs. If there is nothing in the test data
that specifically triggers the filter, it might look like it's running incredibly fast.
Make sure that you have relevant test data for your filter in the
[`spec/fixtures/markdown.md.erb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/fixtures/markdown.md.erb)
file.
## Reading from files and other data sources
Ruby offers several convenience functions that deal with file contents specifically

View File

@ -767,12 +767,14 @@ describe '#show', :snowplow do
expect_snowplow_event(
category: 'Experiment',
action: 'start',
standard_context: { namespace: group, project: project }
)
expect_snowplow_event(
category: 'Experiment',
action: 'sent',
property: 'property',
label: 'label'
label: 'label',
standard_context: { namespace: group, project: project }
)
end
end

View File

@ -77,7 +77,20 @@ in the **Authorized applications** section under **Profile Settings > Applicatio
![Authorized_applications](img/oauth_provider_authorized_application.png)
The GitLab OAuth applications support scopes, which allow various actions that any given
application can perform such as `read_user` and `api`. There are many more scopes
available.
application can perform. The available scopes are depicted in the following table.
| Scope | Description |
| ------------------ | ----------- |
| `api` | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. |
| `read_user` | Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. |
| `read_api` | Grants read access to the API, including all groups and projects, the container registry, and the package registry. |
| `read_repository` | Grants read-only access to repositories on private projects using Git-over-HTTP or the Repository Files API. |
| `write_repository` | Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API). |
| `read_registry` | Grants read-only access to container registry images on private projects. |
| `write_registry` | Grants read-only access to container registry images on private projects. |
| `sudo` | Grants permission to perform API actions as any user in the system, when authenticated as an admin user. |
| `openid` | Grants permission to authenticate with GitLab using [OpenID Connect](openid_connect_provider.md). Also gives read-only access to the user's profile and group memberships. |
| `profile` | Grants read-only access to the user's profile data using [OpenID Connect](openid_connect_provider.md). |
| `email` | Grants read-only access to the user's primary email address using [OpenID Connect](openid_connect_provider.md). |
At any time you can revoke any access by just clicking **Revoke**.

View File

@ -12,12 +12,13 @@ module API
end
params do
requires :id, type: String, desc: 'The suggestion ID'
optional :commit_message, type: String, desc: "A custom commit message to use instead of the default generated message or the project's default message"
end
put ':id/apply' do
suggestion = Suggestion.find_by_id(params[:id])
if suggestion
apply_suggestions(suggestion, current_user)
apply_suggestions(suggestion, current_user, params[:commit_message])
else
render_api_error!(_('Suggestion is not applicable as the suggestion was not found.'), :not_found)
end
@ -28,6 +29,7 @@ module API
end
params do
requires :ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: "An array of suggestion ID's"
optional :commit_message, type: String, desc: "A custom commit message to use instead of the default generated message or the project's default message"
end
put 'batch_apply' do
ids = params[:ids]
@ -35,7 +37,7 @@ module API
suggestions = Suggestion.id_in(ids)
if suggestions.size == ids.length
apply_suggestions(suggestions, current_user)
apply_suggestions(suggestions, current_user, params[:commit_message])
else
render_api_error!(_('Suggestions are not applicable as one or more suggestions were not found.'), :not_found)
end
@ -43,10 +45,10 @@ module API
end
helpers do
def apply_suggestions(suggestions, current_user)
def apply_suggestions(suggestions, current_user, message)
authorize_suggestions(*suggestions)
result = ::Suggestions::ApplyService.new(current_user, *suggestions).execute
result = ::Suggestions::ApplyService.new(current_user, *suggestions, message: message).execute
if result[:status] == :success
present suggestions, with: Entities::Suggestion, current_user: current_user

View File

@ -47,3 +47,5 @@ module Gitlab
end
end
end
Gitlab::AlertManagement::Payload.prepend_if_ee('EE::Gitlab::AlertManagement::Payload')

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module Gitlab
module Changelog
# A class used for committing a release's changelog to a Git repository.
class Committer
CommitError = Class.new(StandardError)
def initialize(project, user)
@project = project
@user = user
end
# Commits a release's changelog to a file on a branch.
#
# The `release` argument is a `Gitlab::Changelog::Release` for which to
# update the changelog.
#
# The `file` argument specifies the path to commit the changes to.
#
# The `branch` argument specifies the branch to commit the changes on.
#
# The `message` argument specifies the commit message to use.
def commit(release:, file:, branch:, message:)
# When retrying, we need to reprocess the existing changelog from
# scratch, otherwise we may end up throwing away changes. As such, all
# the logic is contained within the retry block.
Retriable.retriable(on: CommitError) do
commit = @project.commit(branch)
content = blob_content(file, commit)
# If the release has already been added (e.g. concurrently by another
# API call), we don't want to add it again.
break if content&.match?(release.header_start_pattern)
service = Files::MultiService.new(
@project,
@user,
commit_message: message,
branch_name: branch,
start_branch: branch,
actions: [
{
action: content ? 'update' : 'create',
content: Generator.new(content.to_s).add(release),
file_path: file,
last_commit_id: commit&.sha
}
]
)
result = service.execute
raise CommitError.new(result[:message]) if result[:status] != :success
end
end
def blob_content(file, commit = nil)
return unless commit
@project.repository.blob_at(commit.sha, file)&.data
end
end
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
module Gitlab
module Changelog
# Configuration settings used when generating changelogs.
class Config
ConfigError = Class.new(StandardError)
# When rendering changelog entries, authors are not included.
AUTHORS_NONE = 'none'
# The path to the configuration file as stored in the project's Git
# repository.
FILE_PATH = '.gitlab/changelog_config.yml'
# The default date format to use for formatting release dates.
DEFAULT_DATE_FORMAT = '%Y-%m-%d'
# The default template to use for generating release sections.
DEFAULT_TEMPLATE = File.read(File.join(__dir__, 'template.tpl'))
attr_accessor :date_format, :categories, :template
def self.from_git(project)
if (yaml = project.repository.changelog_config)
from_hash(project, YAML.safe_load(yaml))
else
new(project)
end
end
def self.from_hash(project, hash)
config = new(project)
if (date = hash['date_format'])
config.date_format = date
end
if (template = hash['template'])
config.template = Template::Compiler.new.compile(template)
end
if (categories = hash['categories'])
if categories.is_a?(Hash)
config.categories = categories
else
raise ConfigError, 'The "categories" configuration key must be a Hash'
end
end
config
end
def initialize(project)
@project = project
@date_format = DEFAULT_DATE_FORMAT
@template = Template::Compiler.new.compile(DEFAULT_TEMPLATE)
@categories = {}
end
def contributor?(user)
@project.team.contributor?(user)
end
def category(name)
@categories[name] || name
end
def format_date(date)
date.strftime(@date_format)
end
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
module Gitlab
module Changelog
# Parsing and generating of Markdown changelogs.
class Generator
# The regex used to parse a release header.
RELEASE_REGEX =
/^##\s+(?<version>#{Gitlab::Regex.unbounded_semver_regex})/.freeze
# The `input` argument must be a `String` containing the existing
# changelog Markdown. If no changelog exists, this should be an empty
# `String`.
def initialize(input = '')
@lines = input.lines
@locations = {}
@lines.each_with_index do |line, index|
matches = line.match(RELEASE_REGEX)
next if !matches || !matches[:version]
@locations[matches[:version]] = index
end
end
# Generates the Markdown for the given release and returns the new
# changelog Markdown content.
#
# The `release` argument must be an instance of
# `Gitlab::Changelog::Release`.
def add(release)
versions = [release.version, *@locations.keys]
VersionSorter.rsort!(versions)
new_index = versions.index(release.version)
new_lines = @lines.dup
markdown = release.to_markdown
if (insert_after = versions[new_index + 1])
line_index = @locations[insert_after]
new_lines.insert(line_index, markdown)
else
# When adding to the end of the changelog, the previous section only
# has a single newline, resulting in the release section title
# following it immediately. When this is the case, we insert an extra
# empty line to keep the changelog readable in its raw form.
new_lines.push("\n") if versions.length > 1
new_lines.push(markdown.rstrip)
new_lines.push("\n")
end
new_lines.join
end
end
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
module Gitlab
module Changelog
# A release to add to a changelog.
class Release
attr_reader :version
def initialize(version:, date:, config:)
@version = version
@date = date
@config = config
@entries = Hash.new { |h, k| h[k] = [] }
# This ensures that entries are presented in the same order as the
# categories Hash in the user's configuration.
@config.categories.values.each do |category|
@entries[category] = []
end
end
def add_entry(
title:,
commit:,
category:,
author: nil,
merge_request: nil
)
# When changing these fields, keep in mind that this needs to be
# backwards compatible. For example, you can't just remove a field as
# this will break the changelog generation process for existing users.
entry = {
'title' => title,
'commit' => {
'reference' => commit.to_reference(full: true),
'trailers' => commit.trailers
}
}
if author
entry['author'] = {
'reference' => author.to_reference(full: true),
'contributor' => @config.contributor?(author)
}
end
if merge_request
entry['merge_request'] = {
'reference' => merge_request.to_reference(full: true)
}
end
@entries[@config.category(category)] << entry
end
def to_markdown
# While not critical, we would like release sections to be separated by
# an empty line in the changelog; ensuring it's readable even in its
# raw form.
#
# Since it can be a bit tricky to get this right using Liquid, we
# enforce an empty line separator ourselves.
markdown =
@config.template.render('categories' => entries_for_template).strip
# The release header can't be changed using the Liquid template, as we
# need this to be in a known format. Without this restriction, we won't
# know where to insert a new release section in an existing changelog.
"## #{@version} (#{release_date})\n\n#{markdown}\n\n"
end
def header_start_pattern
/^##\s*#{Regexp.escape(@version)}/
end
private
def release_date
@config.format_date(@date)
end
def entries_for_template
@entries.map do |category, entries|
{
'title' => category,
'count' => entries.length,
'single_change' => entries.length == 1,
'entries' => entries
}
end
end
end
end
end

View File

@ -0,0 +1,14 @@
{% if categories %}
{% each categories %}
### {{ title }} ({% if single_change %}1 change{% else %}{{ count }} changes{% end %})
{% each entries %}
- [{{ title }}]({{ commit.reference }})\
{% if author.contributor %} by {{ author.reference }}{% end %}\
{% if merge_request %} ([merge request]({{ merge_request.reference }})){% end %}
{% end %}
{% end %}
{% else %}
No changes.
{% end %}

View File

@ -0,0 +1,146 @@
# frozen_string_literal: true
module Gitlab
module Changelog
module Template
# Compiler is used for turning a minimal user templating language into an
# ERB template, without giving the user access to run arbitrary code.
#
# The template syntax is deliberately made as minimal as possible, and
# only supports the following:
#
# * Printing a value
# * Iterating over collections
# * if/else
#
# The syntax looks as follows:
#
# {% each users %}
#
# Name: {{user}}
# Likes cats: {% if likes_cats %}yes{% else %}no{% end %}
#
# {% end %}
#
# Newlines can be escaped by ending a line with a backslash. So this:
#
# foo \
# bar
#
# Is the same as this:
#
# foo bar
#
# Templates are compiled into ERB templates, while taking care to make
# sure the user can't run arbitrary code. By using ERB we can let it do
# the heavy lifting of rendering data; all we need to provide is a
# translation layer.
#
# # Security
#
# The template syntax this compiler exposes is safe to be used by
# untrusted users. Not only are they unable to run arbitrary code, the
# compiler also enforces a limit on the integer sizes and the number of
# nested loops. ERB tags added by the user are also disabled.
class Compiler
# A pattern to match a single integer, with an upper size limit.
#
# We enforce a limit of 10 digits (= a 32 bits integer) so users can't
# trigger the allocation of infinitely large bignums, or trigger
# RangeError errors when using such integers to access an array value.
INTEGER = /^\d{1,10}$/.freeze
# The name/path of a variable, such as `user.address.city`.
#
# It's important that this regular expression _doesn't_ allow for
# anything but letters, numbers, and underscores, otherwise a user may
# use those to "escape" our template and run arbirtary Ruby code. For
# example, take this variable:
#
# {{') ::Kernel.exit #'}}
#
# This would then be compiled into:
#
# <%= read(variables, '') ::Kernel.exit #'') %>
#
# Restricting the allowed characters makes this impossible.
VAR_NAME = /([\w\.]+)/.freeze
# A variable tag, such as `{{username}}`.
VAR = /{{ \s* #{VAR_NAME} \s* }}/x.freeze
# The opening tag for a statement.
STM_START = /{% \s*/x.freeze
# The closing tag for a statement.
STM_END = /\s* %}/x.freeze
# A regular `end` closing tag.
NORMAL_END = /#{STM_START} end #{STM_END}/x.freeze
# An `end` closing tag on its own line, without any non-whitespace
# preceding or following it.
#
# These tags need some special care to make it easier to control
# whitespace.
LONELY_END = /^\s*#{NORMAL_END}\s$/x.freeze
# An `else` tag.
ELSE = /#{STM_START} else #{STM_END}/x.freeze
# The start of an `each` tag.
EACH = /#{STM_START} each \s+ #{VAR_NAME} #{STM_END}/x.freeze
# The start of an `if` tag.
IF = /#{STM_START} if \s+ #{VAR_NAME} #{STM_END}/x.freeze
# The pattern to use for escaping newlines.
ESCAPED_NEWLINE = /\\\n$/.freeze
# The start tag for ERB tags. These tags will be escaped, preventing
# users FROM USING erb DIRECTLY.
ERB_START_TAG = '<%'
def compile(template)
transformed_lines = ['<% it = variables %>']
template.each_line { |line| transformed_lines << transform(line) }
Template.new(transformed_lines.join)
end
def transform(line)
line.gsub!(ESCAPED_NEWLINE, '')
line.gsub!(ERB_START_TAG, '<%%')
# This replacement ensures that "end" blocks on their own lines
# don't add extra newlines. Using an ERB -%> tag sadly swallows too
# many newlines.
line.gsub!(LONELY_END, '<% end %>')
line.gsub!(NORMAL_END, '<% end %>')
line.gsub!(ELSE, '<% else -%>')
line.gsub!(EACH) do
# No, `it; variables` isn't a syntax error. Using `;` marks
# `variables` as block-local, making it possible to re-assign it
# without affecting outer definitions of this variable. We use
# this to scope template variables to the right input Hash.
"<% each(#{read_path(Regexp.last_match(1))}) do |it; variables| -%><% variables = it -%>"
end
line.gsub!(IF) { "<% if truthy?(#{read_path(Regexp.last_match(1))}) -%>" }
line.gsub!(VAR) { "<%= #{read_path(Regexp.last_match(1))} %>" }
line
end
def read_path(path)
return path if path == 'it'
args = path.split('.')
args.map! { |arg| arg.match?(INTEGER) ? "#{arg}" : "'#{arg}'" }
"read(variables, #{args.join(', ')})"
end
end
end
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
module Gitlab
module Changelog
module Template
# Context is used to provide a binding/context to ERB templates used for
# rendering changelogs.
#
# This class extends BasicObject so that we only expose the bare minimum
# needed to render the ERB template.
class Context < BasicObject
MAX_NESTED_LOOPS = 4
def initialize(variables)
@variables = variables
@loop_nesting = 0
end
def get_binding
::Kernel.binding
end
def each(value, &block)
max = MAX_NESTED_LOOPS
if @loop_nesting == max
::Kernel.raise(
::Template::TemplateError.new("You can only nest up to #{max} loops")
)
end
@loop_nesting += 1
result = value.each(&block) if value.respond_to?(:each)
@loop_nesting -= 1
result
end
# rubocop: disable Style/TrivialAccessors
def variables
@variables
end
# rubocop: enable Style/TrivialAccessors
def read(source, *steps)
current = source
steps.each do |step|
case current
when ::Hash
current = current[step]
when ::Array
return '' unless step.is_a?(::Integer)
current = current[step]
else
break
end
end
current
end
def truthy?(value)
value.respond_to?(:any?) ? value.any? : !!value
end
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Gitlab
module Changelog
module Template
# A wrapper around an ERB template user for rendering changelogs.
class Template
TemplateError = Class.new(StandardError)
def initialize(erb)
# Don't change the trim mode, as this may require changes to the
# regular expressions used to turn the template syntax into ERB
# tags.
@erb = ERB.new(erb, trim_mode: '-')
end
def render(data)
context = Context.new(data).get_binding
# ERB produces a SyntaxError when processing templates, as it
# internally uses eval() for this.
@erb.result(context)
rescue SyntaxError
raise TemplateError.new("The template's syntax is invalid")
end
end
end
end
end

View File

@ -6,14 +6,15 @@ module Gitlab
DEFAULT_SUGGESTION_COMMIT_MESSAGE =
'Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)'
def initialize(user, suggestion_set)
def initialize(user, suggestion_set, custom_message = nil)
@user = user
@suggestion_set = suggestion_set
@custom_message = custom_message
end
def message
project = suggestion_set.project
user_defined_message = project.suggestion_commit_message.presence
user_defined_message = @custom_message.presence || project.suggestion_commit_message.presence
message = user_defined_message || DEFAULT_SUGGESTION_COMMIT_MESSAGE
Gitlab::StringPlaceholderReplacer

View File

@ -1927,9 +1927,6 @@ msgstr ""
msgid "AdminArea|Features"
msgstr ""
msgid "AdminArea|Groups: %{number_of_groups}"
msgstr ""
msgid "AdminArea|Guest"
msgstr ""
@ -1963,7 +1960,7 @@ msgstr ""
msgid "AdminArea|Owner"
msgstr ""
msgid "AdminArea|Projects: %{number_of_projects}"
msgid "AdminArea|Projects"
msgstr ""
msgid "AdminArea|Reporter"
@ -1987,6 +1984,9 @@ msgstr ""
msgid "AdminArea|User cap"
msgstr ""
msgid "AdminArea|Users"
msgstr ""
msgid "AdminArea|Users statistics"
msgstr ""
@ -1996,7 +1996,13 @@ msgstr ""
msgid "AdminArea|Users without a Group and Project"
msgstr ""
msgid "AdminArea|Users: %{number_of_users}"
msgid "AdminArea|View latest groups"
msgstr ""
msgid "AdminArea|View latest projects"
msgstr ""
msgid "AdminArea|View latest users"
msgstr ""
msgid "AdminArea|Youre about to stop all jobs.This will halt all current jobs that are running."
@ -4445,7 +4451,7 @@ msgstr ""
msgid "Bi-weekly code coverage"
msgstr ""
msgid "Billable Users:"
msgid "Billable Users"
msgstr ""
msgid "Billing"
@ -4466,7 +4472,7 @@ msgstr ""
msgid "BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}."
msgstr ""
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}, or start a free 30-day trial of GitLab.com Gold."
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}, or start a free 30-day trial of GitLab.com Ultimate."
msgstr ""
msgid "BillingPlans|Learn more about each plan by visiting our %{pricing_page_link}."
@ -8552,7 +8558,7 @@ msgstr ""
msgid "CurrentUser|Settings"
msgstr ""
msgid "CurrentUser|Start a Gold trial"
msgid "CurrentUser|Start an Ultimate trial"
msgstr ""
msgid "CurrentUser|Upgrade"
@ -15281,6 +15287,9 @@ msgstr ""
msgid "Instance administrators group already exists"
msgstr ""
msgid "Instance overview"
msgstr ""
msgid "InstanceStatistics|Could not load the issues and merge requests chart. Please refresh the page to try again."
msgstr ""
@ -16678,7 +16687,7 @@ msgstr ""
msgid "Learn GitLab"
msgstr ""
msgid "Learn GitLab - Gold trial"
msgid "Learn GitLab - Ultimate trial"
msgstr ""
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
@ -17460,7 +17469,7 @@ msgstr ""
msgid "Maximum PyPI package file size in bytes"
msgstr ""
msgid "Maximum Users:"
msgid "Maximum Users"
msgstr ""
msgid "Maximum allowable lifetime for personal access token (days)"
@ -26968,7 +26977,7 @@ msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
msgid "Start a Free Gold Trial"
msgid "Start a Free Ultimate Trial"
msgstr ""
msgid "Start a new discussion..."
@ -27010,7 +27019,7 @@ msgstr ""
msgid "Start thread & reopen %{noteable_name}"
msgstr ""
msgid "Start your Free Gold Trial"
msgid "Start your Free Ultimate Trial"
msgstr ""
msgid "Start your free trial"
@ -30077,7 +30086,7 @@ msgid_plural "Trials|%{plan} Trial %{en_dash} %{num} days left"
msgstr[0] ""
msgstr[1] ""
msgid "Trials|Create a new group to start your GitLab Gold trial."
msgid "Trials|Create a new group to start your GitLab Ultimate trial."
msgstr ""
msgid "Trials|Go back to GitLab"
@ -30089,7 +30098,7 @@ msgstr ""
msgid "Trials|Skip Trial (Continue with Free Account)"
msgstr ""
msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start a Gold trial'"
msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start an Ultimate trial'"
msgstr ""
msgid "Trials|You can apply your trial to a new group or an existing group."
@ -30116,7 +30125,7 @@ msgstr ""
msgid "Trial|Dismiss"
msgstr ""
msgid "Trial|GitLab Gold trial (optional)"
msgid "Trial|GitLab Ultimate trial (optional)"
msgstr ""
msgid "Trial|How many employees will use Gitlab?"
@ -30131,7 +30140,7 @@ msgstr ""
msgid "Trial|Telephone number"
msgstr ""
msgid "Trial|Upgrade to Gold to keep using GitLab with advanced features."
msgid "Trial|Upgrade to Ultimate to keep using GitLab with advanced features."
msgstr ""
msgid "Trial|We will activate your trial on your group after you complete this step. After 30 days, you can:"
@ -31166,13 +31175,10 @@ msgstr ""
msgid "Users in License"
msgstr ""
msgid "Users in License:"
msgstr ""
msgid "Users or groups set as approvers in the project's or merge request's settings."
msgstr ""
msgid "Users over License:"
msgid "Users over License"
msgstr ""
msgid "Users requesting access to"
@ -32900,7 +32906,7 @@ msgstr ""
msgid "Your GitLab group"
msgstr ""
msgid "Your Gitlab Gold trial will last 30 days after which point you can keep your free Gitlab account forever. We just need some additional information to activate your trial."
msgid "Your Gitlab Ultimate trial will last 30 days after which point you can keep your free Gitlab account forever. We just need some additional information to activate your trial."
msgstr ""
msgid "Your Groups"

View File

@ -306,66 +306,6 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
end
describe 'tracking group creation for onboarding issues experiment' do
before do
sign_in(user)
end
subject(:create_namespace) { post :create, params: { group: { name: 'new_group', path: 'new_group' } } }
context 'experiment disabled' do
before do
stub_experiment(onboarding_issues: false)
end
it 'does not track anything', :snowplow do
create_namespace
expect_no_snowplow_event
end
end
context 'experiment enabled' do
before do
stub_experiment(onboarding_issues: true)
end
context 'and the user is part of the control group' do
before do
stub_experiment_for_subject(onboarding_issues: false)
end
it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do
create_namespace
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything,
property: 'control_group'
)
end
end
context 'and the user is part of the experimental group' do
before do
stub_experiment_for_subject(onboarding_issues: true)
end
it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do
create_namespace
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything,
property: 'experimental_group'
)
end
end
end
end
end
describe 'GET #index' do

View File

@ -0,0 +1,90 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Changelog::Committer do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:committer) { described_class.new(project, user) }
let(:config) { Gitlab::Changelog::Config.new(project) }
describe '#commit' do
context "when the release isn't in the changelog" do
it 'commits the changes' do
release = Gitlab::Changelog::Release
.new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
committer.commit(
release: release,
file: 'CHANGELOG.md',
branch: 'master',
message: 'Test commit'
)
content = project.repository.blob_at('master', 'CHANGELOG.md').data
expect(content).to eq(<<~MARKDOWN)
## 1.0.0 (2020-01-01)
No changes.
MARKDOWN
end
end
context 'when the release is already in the changelog' do
it "doesn't commit the changes" do
release = Gitlab::Changelog::Release
.new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
2.times do
committer.commit(
release: release,
file: 'CHANGELOG.md',
branch: 'master',
message: 'Test commit'
)
end
content = project.repository.blob_at('master', 'CHANGELOG.md').data
expect(content).to eq(<<~MARKDOWN)
## 1.0.0 (2020-01-01)
No changes.
MARKDOWN
end
end
context 'when committing the changes fails' do
it 'retries the operation' do
release = Gitlab::Changelog::Release
.new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
service = instance_spy(Files::MultiService)
errored = false
allow(Files::MultiService)
.to receive(:new)
.and_return(service)
allow(service).to receive(:execute) do
if errored
{ status: :success }
else
errored = true
{ status: :error }
end
end
expect do
committer.commit(
release: release,
file: 'CHANGELOG.md',
branch: 'master',
message: 'Test commit'
)
end.not_to raise_error
end
end
end
end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Changelog::Config do
let(:project) { build_stubbed(:project) }
describe '.from_git' do
it 'retrieves the configuration from Git' do
allow(project.repository)
.to receive(:changelog_config)
.and_return("---\ndate_format: '%Y'")
expect(described_class)
.to receive(:from_hash)
.with(project, 'date_format' => '%Y')
described_class.from_git(project)
end
it 'returns the default configuration when no YAML file exists in Git' do
allow(project.repository)
.to receive(:changelog_config)
.and_return(nil)
expect(described_class)
.to receive(:new)
.with(project)
described_class.from_git(project)
end
end
describe '.from_hash' do
it 'sets the configuration according to a Hash' do
config = described_class.from_hash(
project,
'date_format' => 'foo',
'template' => 'bar',
'categories' => { 'foo' => 'bar' }
)
expect(config.date_format).to eq('foo')
expect(config.template).to be_instance_of(Gitlab::Changelog::Template::Template)
expect(config.categories).to eq({ 'foo' => 'bar' })
end
it 'raises ConfigError when the categories are not a Hash' do
expect { described_class.from_hash(project, 'categories' => 10) }
.to raise_error(described_class::ConfigError)
end
end
describe '#contributor?' do
it 'returns true if a user is a contributor' do
user = build_stubbed(:author)
allow(project.team).to receive(:contributor?).with(user).and_return(true)
expect(described_class.new(project).contributor?(user)).to eq(true)
end
it "returns true if a user isn't a contributor" do
user = build_stubbed(:author)
allow(project.team).to receive(:contributor?).with(user).and_return(false)
expect(described_class.new(project).contributor?(user)).to eq(false)
end
end
describe '#category' do
it 'returns the name of a category' do
config = described_class.new(project)
config.categories['foo'] = 'Foo'
expect(config.category('foo')).to eq('Foo')
end
it 'returns the raw category name when no alternative name is configured' do
config = described_class.new(project)
expect(config.category('bla')).to eq('bla')
end
end
describe '#format_date' do
it 'formats a date according to the configured date format' do
config = described_class.new(project)
time = Time.utc(2021, 1, 5)
expect(config.format_date(time)).to eq('2021-01-05')
end
end
end

View File

@ -0,0 +1,164 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Changelog::Generator do
describe '#add' do
let(:project) { build_stubbed(:project) }
let(:author) { build_stubbed(:user) }
let(:commit) { build_stubbed(:commit) }
let(:config) { Gitlab::Changelog::Config.new(project) }
it 'generates the Markdown for the first release' do
release = Gitlab::Changelog::Release.new(
version: '1.0.0',
date: Time.utc(2021, 1, 5),
config: config
)
release.add_entry(
title: 'This is a new change',
commit: commit,
category: 'added',
author: author
)
gen = described_class.new('')
expect(gen.add(release)).to eq(<<~MARKDOWN)
## 1.0.0 (2021-01-05)
### added (1 change)
- [This is a new change](#{commit.to_reference(full: true)})
MARKDOWN
end
it 'generates the Markdown for a newer release' do
release = Gitlab::Changelog::Release.new(
version: '2.0.0',
date: Time.utc(2021, 1, 5),
config: config
)
release.add_entry(
title: 'This is a new change',
commit: commit,
category: 'added',
author: author
)
gen = described_class.new(<<~MARKDOWN)
This is a changelog file.
## 1.0.0
This is the changelog for version 1.0.0.
MARKDOWN
expect(gen.add(release)).to eq(<<~MARKDOWN)
This is a changelog file.
## 2.0.0 (2021-01-05)
### added (1 change)
- [This is a new change](#{commit.to_reference(full: true)})
## 1.0.0
This is the changelog for version 1.0.0.
MARKDOWN
end
it 'generates the Markdown for a patch release' do
release = Gitlab::Changelog::Release.new(
version: '1.1.0',
date: Time.utc(2021, 1, 5),
config: config
)
release.add_entry(
title: 'This is a new change',
commit: commit,
category: 'added',
author: author
)
gen = described_class.new(<<~MARKDOWN)
This is a changelog file.
## 2.0.0
This is another release.
## 1.0.0
This is the changelog for version 1.0.0.
MARKDOWN
expect(gen.add(release)).to eq(<<~MARKDOWN)
This is a changelog file.
## 2.0.0
This is another release.
## 1.1.0 (2021-01-05)
### added (1 change)
- [This is a new change](#{commit.to_reference(full: true)})
## 1.0.0
This is the changelog for version 1.0.0.
MARKDOWN
end
it 'generates the Markdown for an old release' do
release = Gitlab::Changelog::Release.new(
version: '0.5.0',
date: Time.utc(2021, 1, 5),
config: config
)
release.add_entry(
title: 'This is a new change',
commit: commit,
category: 'added',
author: author
)
gen = described_class.new(<<~MARKDOWN)
This is a changelog file.
## 2.0.0
This is another release.
## 1.0.0
This is the changelog for version 1.0.0.
MARKDOWN
expect(gen.add(release)).to eq(<<~MARKDOWN)
This is a changelog file.
## 2.0.0
This is another release.
## 1.0.0
This is the changelog for version 1.0.0.
## 0.5.0 (2021-01-05)
### added (1 change)
- [This is a new change](#{commit.to_reference(full: true)})
MARKDOWN
end
end
end

View File

@ -0,0 +1,107 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Changelog::Release do
describe '#to_markdown' do
let(:config) { Gitlab::Changelog::Config.new(build_stubbed(:project)) }
let(:commit) { build_stubbed(:commit) }
let(:author) { build_stubbed(:user) }
let(:mr) { build_stubbed(:merge_request) }
let(:release) do
described_class
.new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config)
end
context 'when there are no entries' do
it 'includes a notice about the lack of entries' do
expect(release.to_markdown).to eq(<<~OUT)
## 1.0.0 (2021-01-05)
No changes.
OUT
end
end
context 'when all data is present' do
it 'includes all data' do
allow(config).to receive(:contributor?).with(author).and_return(true)
release.add_entry(
title: 'Entry title',
commit: commit,
category: 'fixed',
author: author,
merge_request: mr
)
expect(release.to_markdown).to eq(<<~OUT)
## 1.0.0 (2021-01-05)
### fixed (1 change)
- [Entry title](#{commit.to_reference(full: true)}) \
by #{author.to_reference(full: true)} \
([merge request](#{mr.to_reference(full: true)}))
OUT
end
end
context 'when no merge request is present' do
it "doesn't include a merge request link" do
allow(config).to receive(:contributor?).with(author).and_return(true)
release.add_entry(
title: 'Entry title',
commit: commit,
category: 'fixed',
author: author
)
expect(release.to_markdown).to eq(<<~OUT)
## 1.0.0 (2021-01-05)
### fixed (1 change)
- [Entry title](#{commit.to_reference(full: true)}) \
by #{author.to_reference(full: true)}
OUT
end
end
context 'when the author is not a contributor' do
it "doesn't include the author" do
allow(config).to receive(:contributor?).with(author).and_return(false)
release.add_entry(
title: 'Entry title',
commit: commit,
category: 'fixed',
author: author
)
expect(release.to_markdown).to eq(<<~OUT)
## 1.0.0 (2021-01-05)
### fixed (1 change)
- [Entry title](#{commit.to_reference(full: true)})
OUT
end
end
end
describe '#header_start_position' do
it 'returns a regular expression for finding the start of a release section' do
config = Gitlab::Changelog::Config.new(build_stubbed(:project))
release = described_class
.new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config)
expect(release.header_start_pattern).to eq(/^##\s*1\.0\.0/)
end
end
end

View File

@ -0,0 +1,129 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Changelog::Template::Compiler do
def compile(template, data = {})
Gitlab::Changelog::Template::Compiler.new.compile(template).render(data)
end
describe '#compile' do
it 'compiles an empty template' do
expect(compile('')).to eq('')
end
it 'compiles a template with an undefined variable' do
expect(compile('{{number}}')).to eq('')
end
it 'compiles a template with a defined variable' do
expect(compile('{{number}}', 'number' => 42)).to eq('42')
end
it 'compiles a template with the special "it" variable' do
expect(compile('{{it}}', 'values' => 10)).to eq({ 'values' => 10 }.to_s)
end
it 'compiles a template containing an if statement' do
expect(compile('{% if foo %}yes{% end %}', 'foo' => true)).to eq('yes')
end
it 'compiles a template containing an if/else statement' do
expect(compile('{% if foo %}yes{% else %}no{% end %}', 'foo' => false))
.to eq('no')
end
it 'compiles a template that iterates over an Array' do
expect(compile('{% each numbers %}{{it}}{% end %}', 'numbers' => [1, 2, 3]))
.to eq('123')
end
it 'compiles a template that iterates over a Hash' do
output = compile(
'{% each pairs %}{{0}}={{1}}{% end %}',
'pairs' => { 'key' => 'value' }
)
expect(output).to eq('key=value')
end
it 'compiles a template that iterates over a Hash of Arrays' do
output = compile(
'{% each values %}{{key}}{% end %}',
'values' => [{ 'key' => 'value' }]
)
expect(output).to eq('value')
end
it 'compiles a template with a variable path' do
output = compile('{{foo.bar}}', 'foo' => { 'bar' => 10 })
expect(output).to eq('10')
end
it 'compiles a template with a variable path that uses an Array index' do
output = compile('{{foo.values.1}}', 'foo' => { 'values' => [10, 20] })
expect(output).to eq('20')
end
it 'compiles a template with a variable path that uses a Hash and a numeric index' do
output = compile('{{foo.1}}', 'foo' => { 'key' => 'value' })
expect(output).to eq('')
end
it 'compiles a template with a variable path that uses an Array and a String based index' do
output = compile('{{foo.numbers.bla}}', 'foo' => { 'numbers' => [10, 20] })
expect(output).to eq('')
end
it 'ignores ERB tags provided by the user' do
input = '<% exit %> <%= exit %> <%= foo -%>'
expect(compile(input)).to eq(input)
end
it 'removes newlines introduced by end statements on their own lines' do
output = compile(<<~TPL, 'foo' => true)
{% if foo %}
foo
{% end %}
TPL
expect(output).to eq("foo\n")
end
it 'supports escaping of trailing newlines' do
output = compile(<<~TPL)
foo \
bar\
baz
TPL
expect(output).to eq("foo barbaz\n")
end
# rubocop: disable Lint/InterpolationCheck
it 'ignores embedded Ruby expressions' do
input = '#{exit}'
expect(compile(input)).to eq(input)
end
# rubocop: enable Lint/InterpolationCheck
it 'ignores ERB tags inside variable tags' do
input = '{{<%= exit %>}}'
expect(compile(input)).to eq(input)
end
it 'ignores malicious code that tries to escape a variable' do
input = "{{') ::Kernel.exit # '}}"
expect(compile(input)).to eq(input)
end
end
end

View File

@ -72,6 +72,17 @@ RSpec.describe Gitlab::Suggestions::CommitMessage do
end
end
context 'when a custom commit message is specified' do
let(:message) { "i'm a project message. a user's custom message takes precedence over me :(" }
let(:custom_message) { "hello there! i'm a cool custom commit message." }
it 'shows the custom commit message' do
expect(Gitlab::Suggestions::CommitMessage
.new(user, suggestion_set, custom_message)
.message).to eq(custom_message)
end
end
context 'is specified and includes all placeholders' do
let(:message) do
'*** %{branch_name} %{files_count} %{file_paths} %{project_name} %{project_path} %{user_full_name} %{username} %{suggestions_count} ***'

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Release do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:release) { create(:release, project: project, author: user) }
let(:release) { create(:release, project: project, author: user) }
it { expect(release).to be_valid }
@ -89,6 +89,61 @@ RSpec.describe Release do
end
end
describe '#update' do
subject { release.update(params) }
context 'when links do not exist' do
context 'when params are specified for creation' do
let(:params) do
{ links_attributes: [{ name: 'test', url: 'https://www.google.com/' }] }
end
it 'creates a link successfuly' do
is_expected.to eq(true)
expect(release.links.count).to eq(1)
expect(release.links.first.name).to eq('test')
expect(release.links.first.url).to eq('https://www.google.com/')
end
end
end
context 'when a link exists' do
let!(:link1) { create(:release_link, release: release, name: 'test1', url: 'https://www.google1.com/') }
let!(:link2) { create(:release_link, release: release, name: 'test2', url: 'https://www.google2.com/') }
before do
release.reload
end
context 'when params are specified for update' do
let(:params) do
{ links_attributes: [{ id: link1.id, name: 'new' }] }
end
it 'updates the link successfully' do
is_expected.to eq(true)
expect(release.links.count).to eq(2)
expect(release.links.first.name).to eq('new')
end
end
context 'when params are specified for deletion' do
let(:params) do
{ links_attributes: [{ id: link1.id, _destroy: true }] }
end
it 'removes the link successfuly' do
is_expected.to eq(true)
expect(release.links.count).to eq(1)
expect(release.links.first.name).to eq(link2.name)
end
end
end
end
describe '#sources' do
subject { release.sources }

View File

@ -309,10 +309,7 @@ RSpec.describe 'Creation of a new release' do
let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } }
let(:assets) { { links: [asset_link_1, asset_link_2] } }
# Right now the raw Postgres error message is sent to the user as the validation message.
# We should catch this validation error and return a nicer message:
# https://gitlab.com/gitlab-org/gitlab/-/issues/277087
it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
it_behaves_like 'errors-as-data with message', %r{Validation failed: Links have duplicate values \(My link\)}
end
context 'when two release assets share the same URL' do
@ -320,8 +317,7 @@ RSpec.describe 'Creation of a new release' do
let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } }
let(:assets) { { links: [asset_link_1, asset_link_2] } }
# Same note as above about the ugly error message
it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
it_behaves_like 'errors-as-data with message', %r{Validation failed: Links have duplicate values \(https://example.com\)}
end
context 'when the provided tag name is HEAD' do

View File

@ -283,7 +283,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) }
let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
let_it_be(:release, reload: true) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
let_it_be(:release_link_1) { create(:release_link, release: release) }
let_it_be(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) }
@ -324,7 +324,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) }
let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
let_it_be(:release, reload: true) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
let_it_be(:release_link_1) { create(:release_link, release: release) }
let_it_be(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) }

View File

@ -65,6 +65,19 @@ RSpec.describe API::Suggestions do
end
end
context 'when a custom commit message is included' do
it 'renders an ok response and returns json content' do
project.add_maintainer(user)
message = "cool custom commit message!"
put api(url, user), params: { commit_message: message }
expect(response).to have_gitlab_http_status(:ok)
expect(project.repository.commit.message).to eq(message)
end
end
context 'when not able to apply patch' do
let(:url) { "/suggestions/#{unappliable_suggestion.id}/apply" }
@ -113,9 +126,11 @@ RSpec.describe API::Suggestions do
let(:url) { "/suggestions/batch_apply" }
context 'when successfully applies multiple patches as a batch' do
it 'renders an ok response and returns json content' do
before do
project.add_maintainer(user)
end
it 'renders an ok response and returns json content' do
put api(url, user), params: { ids: [suggestion.id, suggestion2.id] }
expect(response).to have_gitlab_http_status(:ok)
@ -123,6 +138,16 @@ RSpec.describe API::Suggestions do
'appliable', 'applied',
'from_content', 'to_content'))
end
it 'provides a custom commit message' do
message = "cool custom commit message!"
put api(url, user), params: { ids: [suggestion.id, suggestion2.id],
commit_message: message }
expect(response).to have_gitlab_http_status(:ok)
expect(project.repository.commit.message).to eq(message)
end
end
context 'when not able to apply one or more of the patches' do

View File

@ -32,8 +32,8 @@ RSpec.describe Suggestions::ApplyService do
create(:suggestion, :content_from_repo, suggestion_args)
end
def apply(suggestions)
result = apply_service.new(user, *suggestions).execute
def apply(suggestions, custom_message = nil)
result = apply_service.new(user, *suggestions, message: custom_message).execute
suggestions.map { |suggestion| suggestion.reload }
@ -111,6 +111,16 @@ RSpec.describe Suggestions::ApplyService do
end
end
end
context 'with a user suggested commit message' do
let(:message) { "i'm a custom commit message!" }
it "uses the user's commit message" do
apply(suggestions, message)
expect(project.repository.commit.message).to(eq(message))
end
end
end
subject(:apply_service) { described_class }

View File

@ -46,7 +46,7 @@ module SnowplowHelpers
# }
# ]
# )
def expect_snowplow_event(category:, action:, context: nil, **kwargs)
def expect_snowplow_event(category:, action:, context: nil, standard_context: nil, **kwargs)
if context
kwargs[:context] = []
context.each do |c|
@ -56,6 +56,14 @@ module SnowplowHelpers
end
end
if standard_context
expect(Gitlab::Tracking::StandardContext)
.to have_received(:new)
.with(**standard_context)
kwargs[:standard_context] = an_instance_of(Gitlab::Tracking::StandardContext)
end
expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action, **kwargs).at_least(:once)
end

View File

@ -18,6 +18,7 @@ RSpec.configure do |config|
stub_application_setting(snowplow_enabled: true)
allow(SnowplowTracker::SelfDescribingJson).to receive(:new).and_call_original
allow(Gitlab::Tracking::StandardContext).to receive(:new).and_call_original
allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking
end

View File

@ -2,13 +2,16 @@
require 'spec_helper'
RSpec.describe VariableDuplicatesValidator do
let(:validator) { described_class.new(attributes: [:variables], **options) }
RSpec.describe NestedAttributesDuplicatesValidator do
let(:validator) { described_class.new(attributes: [attribute], **options) }
describe '#validate_each' do
let(:project) { build(:project) }
let(:record) { project }
let(:attribute) { :variables }
let(:value) { project.variables }
subject { validator.validate_each(project, :variables, project.variables) }
subject { validator.validate_each(record, attribute, value) }
context 'with no scope' do
let(:options) { {} }
@ -65,5 +68,46 @@ RSpec.describe VariableDuplicatesValidator do
end
end
end
context 'with a child attribute' do
let(:release) { build(:release) }
let(:first_link) { build(:release_link, name: 'test1', url: 'https://www.google1.com', release: release) }
let(:second_link) { build(:release_link, name: 'test2', url: 'https://www.google2.com', release: release) }
let(:record) { release }
let(:attribute) { :links }
let(:value) { release.links }
let(:options) { { scope: :release, child_attributes: %i[name url] } }
before do
release.links << first_link
release.links << second_link
end
it 'does not have any errors' do
subject
expect(release.errors.empty?).to be true
end
context 'when name is duplicated' do
let(:second_link) { build(:release_link, name: 'test1', release: release) }
it 'has a duplicate error' do
subject
expect(release.errors).to have_key(attribute)
end
end
context 'when url is duplicated' do
let(:second_link) { build(:release_link, url: 'https://www.google1.com', release: release) }
it 'has a duplicate error' do
subject
expect(release.errors).to have_key(attribute)
end
end
end
end
end