Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-22 18:11:12 +00:00
parent b6abc9850e
commit 1862f4a83e
72 changed files with 740 additions and 232 deletions

View file

@ -550,8 +550,6 @@
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
changes: *backend-patterns
- <<: *if-security-merge-request
@ -579,8 +577,6 @@
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *workhorse-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
- <<: *if-security-merge-request
@ -629,8 +625,6 @@
######################
.build-images:rules:build-qa-image:
rules:
- <<: *if-not-ee
when: never
- <<: *if-not-canonical-namespace
when: never
- <<: *if-merge-request-targeting-stable-branch
@ -768,8 +762,6 @@
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *startup-css-patterns
- <<: *if-merge-request
changes: *ci-patterns
.frontend:rules:frontend_fixture-as-if-foss:
rules:
@ -906,8 +898,6 @@
changes: *code-qa-patterns
- <<: *if-merge-request-labels-as-if-foss
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
.qa:rules:internal-as-if-foss:
rules:
@ -1009,8 +999,6 @@
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
# When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- <<: *if-merge-request
@ -1112,8 +1100,6 @@
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
# When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- <<: *if-merge-request
@ -1201,8 +1187,6 @@
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
# When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
- <<: *if-merge-request-labels-as-if-foss
@ -1416,9 +1400,6 @@
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true" || $RETRY_FAILED_TESTS_IN_NEW_PROCESS == "true"'
changes: *code-backstage-patterns
when: always
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true" || $RETRY_FAILED_TESTS_IN_NEW_PROCESS == "true"'
changes: *ci-patterns
when: always
#########################
# Static analysis rules #

View file

@ -60,6 +60,15 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
});
};
/**
* Return the given element's offset height, or 0 if the element doesn't exist.
* Probably not useful outside of handleLocationHash.
*
* @param {HTMLElement} element The element to measure.
* @returns {number} The element's offset height.
*/
const getElementOffsetHeight = (element) => element?.offsetHeight ?? 0;
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => {
@ -84,40 +93,26 @@ export const handleLocationHash = () => {
const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
}
if (fixedTabs) {
adjustment -= fixedTabs.offsetHeight;
}
if (fixedDiffStats) {
adjustment -= fixedDiffStats.offsetHeight;
}
if (performanceBar) {
adjustment -= performanceBar.offsetHeight;
}
if (diffFileHeader) {
adjustment -= diffFileHeader.offsetHeight;
}
if (versionMenusContainer) {
adjustment -= versionMenusContainer.offsetHeight;
}
adjustment -= getElementOffsetHeight(fixedNav);
adjustment -= getElementOffsetHeight(fixedTabs);
adjustment -= getElementOffsetHeight(fixedDiffStats);
adjustment -= getElementOffsetHeight(performanceBar);
adjustment -= getElementOffsetHeight(diffFileHeader);
adjustment -= getElementOffsetHeight(versionMenusContainer);
if (isInIssuePage()) {
adjustment -= fixedIssuableTitle.offsetHeight;
adjustment -= getElementOffsetHeight(fixedIssuableTitle);
}
if (isInMRPage()) {
adjustment -= topPadding;
}
if (target?.scrollIntoView) {
target.scrollIntoView(true);
}
setTimeout(() => {
window.scrollBy(0, adjustment);
});

View file

@ -249,7 +249,7 @@ export default {
this.updateContent(params);
this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs });
this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs, property: scope });
},
successCallback(resp) {
// Because we are polling & the user is interacting verify if the response received

View file

@ -34,13 +34,14 @@ module Boards
end
def create
service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params)
issue = service.execute
result = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params).execute
if issue.valid?
render json: serialize_as_json(issue)
if result.success?
render json: serialize_as_json(result[:issue])
elsif result[:issue]
render json: result[:issue].errors, status: :unprocessable_entity
else
render json: issue.errors, status: :unprocessable_entity
render json: result.errors, status: result.http_status || 422
end
end

View file

@ -147,19 +147,26 @@ class Projects::IssuesController < Projects::ApplicationController
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params, spam_params: spam_params)
@issue = service.execute
result = service.execute
create_vulnerability_issue_feedback(issue)
if service.discussions_to_resolve.count(&:resolved?) > 0
flash[:notice] = if service.discussion_to_resolve_id
_("Resolved 1 discussion.")
else
_("Resolved all discussions.")
end
# Only irrecoverable errors such as unauthorized user won't contain an issue in the response
if result.error? && result[:issue].blank?
render_by_create_result_error(result) && return
end
if @issue.valid?
@issue = result[:issue]
if result.success?
create_vulnerability_issue_feedback(@issue)
if service.discussions_to_resolve.count(&:resolved?) > 0
flash[:notice] = if service.discussion_to_resolve_id
_("Resolved 1 discussion.")
else
_("Resolved all discussions.")
end
end
redirect_to project_issue_path(@project, @issue)
else
# NOTE: this CAPTCHA support method is indirectly included via IssuableActions
@ -372,6 +379,21 @@ class Projects::IssuesController < Projects::ApplicationController
private
def render_by_create_result_error(result)
Gitlab::AppLogger.warn(
message: 'Cannot create issue',
errors: result.errors,
http_status: result.http_status
)
error_method_name = "render_#{result.http_status}".to_sym
if respond_to?(error_method_name, true)
send(error_method_name) # rubocop:disable GitlabSecurity/PublicSend
else
render_404
end
end
def clean_params(all_params)
issue_type = all_params[:issue_type].to_s
all_params.delete(:issue_type) unless WorkItems::Type.allowed_types_for_issues.include?(issue_type)

View file

@ -24,8 +24,8 @@ module Mutations
def prepare_response(alert, result)
{
alert: alert,
issue: result.payload[:issue],
errors: Array(result.message)
issue: result[:issue],
errors: result.errors
}
end
end

View file

@ -83,13 +83,13 @@ module Mutations
params = build_create_issue_params(attributes.merge(author_id: current_user.id), project)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
result = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
check_spam_action_response!(issue)
check_spam_action_response!(result[:issue]) if result[:issue]
{
issue: issue.valid? ? issue : nil,
errors: errors_on_object(issue)
issue: result.success? ? result[:issue] : nil,
errors: result.errors
}
end

View file

@ -49,6 +49,6 @@ module SessionsHelper
match = regex.match(email)
return email unless match
match[1] + '*' * match[2].length + match[3] + '*' * match[4].length + match[5]
match[1] + '*' * (match[2] || '').length + match[3] + '*' * (match[4] || '').length + match[5]
end
end

View file

@ -48,22 +48,19 @@ class Repository
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count readme_path contribution_guide
changelog license_blob license_key gitignore
changelog license_blob license_licensee license_gitaly gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
has_visible_content? issue_template_names_hash merge_request_template_names_hash
user_defined_metrics_dashboard_paths xcode_project? has_ambiguous_refs?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license).freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
readme: %i(readme_path),
changelog: :changelog,
license: %i(license_blob license_key license),
license: %i(license_blob license_licensee license_gitaly),
contributing: :contribution_guide,
gitignore: :gitignore,
gitlab_ci: :gitlab_ci_yml,
@ -650,25 +647,30 @@ class Repository
cache_method :license_blob
def license_key
return unless exists?
raw_repository.license_short_name
license&.key
end
cache_method :license_key
def license
return unless license_key
licensee_object = Licensee::License.new(license_key)
return if licensee_object.name.blank?
licensee_object
rescue Licensee::InvalidLicense => e
Gitlab::ErrorTracking.track_exception(e)
nil
if Feature.enabled?(:license_from_gitaly)
license_gitaly
else
license_licensee
end
end
memoize_method :license
def license_licensee
return unless exists?
raw_repository.license(false)
end
cache_method :license_licensee
def license_gitaly
return unless exists?
raw_repository.license(true)
end
cache_method :license_gitaly
def gitignore
file_on_head(:gitignore)

View file

@ -21,7 +21,7 @@ module AlertManagement
result = create_incident
return result unless result.success?
issue = result.payload[:issue]
issue = result[:issue]
perform_after_create_tasks(issue)
result

View file

@ -15,7 +15,7 @@ module IncidentManagement
end
def execute
issue = Issues::CreateService.new(
create_result = Issues::CreateService.new(
project: project,
current_user: current_user,
params: {
@ -29,22 +29,16 @@ module IncidentManagement
).execute
if alert
return error(alert.errors.full_messages.to_sentence, issue) unless alert.valid?
return error(alert.errors.full_messages, create_result[:issue]) unless alert.valid?
end
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
success(issue)
create_result
end
private
attr_reader :title, :description, :severity, :alert
def success(issue)
ServiceResponse.success(payload: { issue: issue })
end
def error(message, issue = nil)
ServiceResponse.error(payload: { issue: issue }, message: message)
end

View file

@ -23,7 +23,7 @@ module Issuable
with_csv_lines.each do |row, line_no|
attributes = issuable_attributes_for(row)
if create_issuable(attributes).persisted?
if create_issuable(attributes)&.persisted?
@results[:success] += 1
else
@results[:error_lines].push(line_no)

View file

@ -75,7 +75,16 @@ module Issues
# Skip creation of system notes for existing attributes of the issue when cloning with notes.
# The system notes of the old issue are copied over so we don't want to end up with duplicate notes.
# When cloning without notes, we want to generate system notes for the attributes that were copied.
CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: with_notes)
create_result = CreateService.new(
project: target_project,
current_user: current_user,
params: new_params,
spam_params: spam_params
).execute(skip_system_notes: with_notes)
raise CloneError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank?
create_result[:issue]
end
def queue_copy_designs

View file

@ -4,6 +4,7 @@ module Issues
class CreateService < Issues::BaseService
include ResolveDiscussions
prepend RateLimitedService
include ::Services::ReturnServiceResponses
rate_limit key: :issues_create,
opts: { scope: [:project, :current_user, :external_author] }
@ -27,7 +28,13 @@ module Issues
@add_related_issue ||= params.delete(:add_related_issue)
filter_resolve_discussion_params
create(@issue, skip_system_notes: skip_system_notes)
issue = create(@issue, skip_system_notes: skip_system_notes)
if issue.persisted?
success(issue: issue)
else
error(issue.errors.full_messages, 422, pass_back: { issue: issue })
end
end
def external_author

View file

@ -14,6 +14,10 @@ module Issues
private
def create_issuable(attributes)
super[:issue]
end
def create_issuable_class
Issues::CreateService
end

View file

@ -83,7 +83,16 @@ module Issues
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
CreateService.new(project: @target_project, current_user: @current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
create_result = CreateService.new(
project: @target_project,
current_user: @current_user,
params: new_params,
spam_params: spam_params
).execute(skip_system_notes: true)
raise MoveError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank?
create_result[:issue]
end
def queue_copy_designs

View file

@ -2,7 +2,6 @@
module WorkItems
class CreateService < Issues::CreateService
include ::Services::ReturnServiceResponses
include WidgetableService
def initialize(project:, current_user: nil, params: {}, spam_params:, widget_params: {})
@ -21,7 +20,10 @@ module WorkItems
return error(_('Operation not allowed'), :forbidden)
end
work_item = super
result = super
return result if result.error?
work_item = result[:issue]
if work_item.valid?
success(payload(work_item))

View file

@ -38,7 +38,7 @@ module IncidentManagement
def log_error(result)
Gitlab::AppLogger.warn(
message: 'Cannot create issue for PagerDuty incident',
issue_errors: result.message
issue_errors: result.errors.join(', ')
)
end
end

View file

@ -37,13 +37,13 @@ module IncidentManagement
end
def log_warning(alert, result)
issue_id = result.payload[:issue]&.id
issue_id = result[:issue]&.id
Gitlab::AppLogger.warn(
message: 'Cannot process an Incident',
issue_id: issue_id,
alert_id: alert.id,
errors: result.message
errors: result.errors.join(', ')
)
end
end

View file

@ -0,0 +1,8 @@
---
name: license_from_gitaly
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77041
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/374300
milestone: '15.5'
type: development
group: group::gitaly
default_enabled: false

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'licensee/license'
module Licensee
module LicensePatch
# Patch from https://github.com/licensee/licensee/pull/589
def ==(other)
other.is_a?(self.class) && key == other.key
end
end
License.prepend LicensePatch
end

View file

@ -0,0 +1,103 @@
- name: "Suggested Reviewers open beta" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
Deciding the right person to [review your merge request](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/) isn't always straightforward or obvious. Choosing the wrong reviewer can cause delays, low quality reviews, back and forth reassigning reviewers, or even no review at all.
Now, GitLab can recommend a reviewer with [Suggested Reviewers](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#suggested-reviewers). Using the changes in a merge request and a project's contribution graph, machine learning powered suggestions appear in the [reviewer dropdown](https://docs.gitlab.com/ee/user/project/merge_requests/getting_started.html#reviewer) in the merge request sidebar.
This feature is currently in [beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta) behind a [feature flag](https://gitlab.com/gitlab-org/gitlab/-/issues/368356). It will be rolling out to all Ultimate GitLab.com customers over the next week.
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: false # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#suggested-reviewers' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/15_4/create-code-review-suggested-reviewers.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y
- name: "Improved CI/CD integration in VS Code" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
When you're constructing complicated GitLab CI configurations that may contain `include:` or `extends:` keywords, it's challenging to ensure the configuration is valid and the resulting file has your expected configuration. Use [GitLab Workflow](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow) for Visual Studio Code to preview your merged GitLab CI/CD configuration file directly in VS Code. You can view your changes locally, and ensure your configuration is as you expect, before you commit and push.
GitLab Workflow [v3.50.0](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main/CHANGELOG.md#3500-2022-09-09) also provides more CI/CD pipeline interactions to help you avoid context-switching:
* Download artifacts: [commit `f4d027c`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/commit/f4d027c616c884bef9fc42e5f20dfac43b811134), [merge request `!635`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/635)
* Retry or cancel an existing pipeline: [commit `c2caee4`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/commit/c2caee40cfcbfb5d13cc790f9a2d1cfcf6c6a7ab), [merge request `!637`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/637)
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: false # Boolean value (true or false)
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://gitlab.com/gitlab-org/gitlab-vscode-extension#show-merged-gitlab-cicd-configuration' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/15_4/create-vs-code-cicd-improvements.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y
- name: "Users on verified domains can bypass email validation" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
New GitLab users created using SAML or SCIM that belong to a [verified domain](https://docs.gitlab.com/ee/user/project/pages/custom_domains_ssl_tls_certification/#1-add-a-custom-domain) no longer receive the GitLab account verification e-mail.
This reduces account activation friction. Accounts generated through a provisioning process are already verified, so users should not have to individually verify them manually.
stage: manage # String value of the stage that the feature was created in. e.g., Growth
self-managed: false # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/group/saml_sso/index.html#bypass-user-verification-with-verified-domains' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/15_4/domain-verification.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y
- name: "Sortable, filterable data-driven tables in Markdown" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
Working with tables in Markdown can be a bit cumbersome. Not only is it difficult to figure out the correct number of pipes and empty cells, but the table output is static when you save your document. If you have to sort the table by the third column in an ascending order, you end up rewriting the whole thing.
Now you can insert data-driven tables using JSON syntax as follows:
1. Write or export a table in JSON.
1. Wrap JSON in a code block that starts with triple backticks `\`` followed by `json:table`.
1. Save your issue, submit your comment, or publish your page.
In the rendered table, you can also enable:
- Sorting for specific fields using `"sortable": true`
- Dynamic filtering of data using `"filter" : true`
Now it's as simple as a click when you have to re-sort that 100-row table and as easy as a web search when you have to find that one issue reference lost in a sea of nearly identical URLs.
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/markdown.html#json' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://img.youtube.com/vi/12yWKw1AdKY/hqdefault.jpg # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y
- name: "Getting started with GitLab Pages just got easier" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
We've made it much easier to get started with GitLab Pages. Instead of creating configuration files by hand, build them interactively using the GitLab UI. Just answer a few basic questions on how your app is built, and we'll build the `.gitlab-ci.yml` file to get you started.
This is the first time we're using our new [Pipeline Wizard](https://docs.gitlab.com/ee/development/cicd/pipeline_wizard.html), a tool that makes it easy to create `.gitlab-ci.yml` files by building them in the GitLab UI. You can look forward to more simplified onboarding helpers like this one.
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/project/pages/getting_started/pages_ui.html' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/15_4/create-pages-onboarding.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y
- name: "More powerful Linux machine types for GitLab SaaS runners" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
When you run jobs on GitLab SaaS Linux runners, you now have access to more powerful machine types: medium and large. With these two machine types, you have more choices for your GitLab SaaS CI/CD jobs. And with 100% job isolation on an ephemeral virtual machine, and security and autoscaling fully managed by GitLab, you can confidently run your critical CI/CD jobs on GitLab SaaS.
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: false # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/15_4/select-multiple-gitlab-saas-linux-runners.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y
- name: "Limit the maximum number of custom domains per project" # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
You can use GitLab Pages to define custom domains for your website. Too many custom domains, however, can result in slow response times from the Pages API and impact the overall reliability of the service. Now you can limit the maximum number of custom domains per project at the instance level and strike the right balance for your needs. The default value is `0` (unlimited).
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: false # Boolean value (true or false)
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/administration/pages/#set-maximum-number-of-gitlab-pages-custom-domains-for-a-project' # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/15_4/create-pages-domain-limits.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2022-09-22 # YYYY-MM-DD
release: 15.4 # XX.Y

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddUniqueIndexOnSbomComponentTypeAndName < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_sbom_components_on_component_type_and_name'
disable_ddl_transaction!
def up
add_concurrent_index :sbom_components, [:component_type, :name], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :sbom_components, name: INDEX_NAME
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddUniqueIndexToSbomComponentVersionsOnComponentIdAndVersion < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_sbom_component_versions_on_component_id_and_version'
disable_ddl_transaction!
def up
add_concurrent_index :sbom_component_versions, [:component_id, :version], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :sbom_component_versions, name: INDEX_NAME
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddUniqueIndexToSbomSourcesOnSourceTypeAndSource < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_sbom_sources_on_source_type_and_source'
disable_ddl_transaction!
def up
add_concurrent_index :sbom_sources, [:source_type, :source], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :sbom_sources, name: INDEX_NAME
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AddUniqueIndexToSbomOccurrencesOnIngestionAttributes < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_sbom_occurrences_on_ingestion_attributes'
ATTRIBUTES = %i[
project_id
component_id
component_version_id
source_id
commit_sha
].freeze
disable_ddl_transaction!
def up
add_concurrent_index :sbom_occurrences, ATTRIBUTES, unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :sbom_occurrences, name: INDEX_NAME
end
end

View file

@ -0,0 +1 @@
35335f40a192889c45f71a8a3b25dd0e7024919ff823b01c2086c0e6959869e0

View file

@ -0,0 +1 @@
a90c4b56f6cf16ec62d4a37e03add702ce8d64640b1c61f6f0b18b2d9720f24e

View file

@ -0,0 +1 @@
f64b85003dde31c4f0ba37cb0b550fb50b8d7753bbae3043f28ed51858349572

View file

@ -0,0 +1 @@
f46a411a6519723cd2ee0a5b287f26f987195ba76e5753febe47502b1152a543

View file

@ -30187,16 +30187,24 @@ CREATE UNIQUE INDEX index_saved_replies_on_name_text_pattern_ops ON saved_replie
CREATE INDEX index_sbom_component_versions_on_component_id ON sbom_component_versions USING btree (component_id);
CREATE UNIQUE INDEX index_sbom_component_versions_on_component_id_and_version ON sbom_component_versions USING btree (component_id, version);
CREATE UNIQUE INDEX index_sbom_components_on_component_type_and_name ON sbom_components USING btree (component_type, name);
CREATE INDEX index_sbom_occurrences_on_component_id ON sbom_occurrences USING btree (component_id);
CREATE INDEX index_sbom_occurrences_on_component_version_id ON sbom_occurrences USING btree (component_version_id);
CREATE UNIQUE INDEX index_sbom_occurrences_on_ingestion_attributes ON sbom_occurrences USING btree (project_id, component_id, component_version_id, source_id, commit_sha);
CREATE INDEX index_sbom_occurrences_on_pipeline_id ON sbom_occurrences USING btree (pipeline_id);
CREATE INDEX index_sbom_occurrences_on_project_id ON sbom_occurrences USING btree (project_id);
CREATE INDEX index_sbom_occurrences_on_source_id ON sbom_occurrences USING btree (source_id);
CREATE UNIQUE INDEX index_sbom_sources_on_source_type_and_source ON sbom_sources USING btree (source_type, source);
CREATE INDEX index_scim_identities_on_group_id ON scim_identities USING btree (group_id);
CREATE UNIQUE INDEX index_scim_identities_on_lower_extern_uid_and_group_id ON scim_identities USING btree (lower((extern_uid)::text), group_id);

View file

@ -206,6 +206,7 @@ This list of limitations only reflects the latest version of GitLab. If you are
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories and files are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accommodate compliance / export control use cases.
- [Pages access control](../../user/project/pages/pages_access_control.md) doesn't work on secondaries. See [GitLab issue #9336](https://gitlab.com/gitlab-org/gitlab/-/issues/9336) for details.
- [GitLab chart with Geo](https://docs.gitlab.com/charts/advanced/geo/) does not support [Unified URLs](secondary_proxy/index.md#set-up-a-unified-url-for-geo-sites). See [GitLab issue #3522](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/3522) for more details.
- [Disaster recovery](disaster_recovery/index.md) for multi-secondary sites causes downtime due to the complete re-synchronization and re-configuration of all non-promoted secondaries.
### Limitations on replication/verification

View file

@ -34,7 +34,7 @@ Possible version history entries are:
> - [Enabled on GitLab.com](issue-link) in GitLab X.X.
> - [Enabled on GitLab.com](issue-link) in GitLab X.X. Available to GitLab.com administrators only.
> - [Enabled on self-managed](issue-link) in GitLab X.X.
> - [Generally available](issue-link) in GitLab X.Y. [Feature flag `flag_name`](issue-link) removed.
> - [Generally available](issue-link) in GitLab X.Y. Feature flag `flag_name` removed.
```
You can combine entries if they happened in the same release:
@ -115,5 +115,5 @@ And, when the feature is done and fully available to all users:
> - Introduced in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Enabled on self-managed](https://gitlab.com/issue/etc) in GitLab 13.8.
> - [Enabled on GitLab.com](https://gitlab.com/issue/etc) in GitLab 13.9.
> - [Generally available](issue-link) in GitLab 14.0. [Feature flag `forti_token_cloud`](issue-link) removed.
> - [Generally available](issue-link) in GitLab 14.0. Feature flag `forti_token_cloud` removed.
```

View file

@ -1,7 +1,7 @@
---
stage: ModelOps
group: Applied ML
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
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: index, reference
---

View file

@ -2,6 +2,7 @@
module API
module Entities
# Serializes a Licensee::License
class License < Entities::LicenseBasic
expose :popular?, as: :popular
expose(:description) { |license| license.meta['description'] }

View file

@ -2,10 +2,16 @@
module API
module Entities
# Serializes a Gitlab::Git::DeclaredLicense
class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
# This was dropped:
# https://github.com/github/choosealicense.com/commit/325806b42aa3d5b78e84120327ec877bc936dbdd#diff-66df8f1997786f7052d29010f2cbb4c66391d60d24ca624c356acc0ab986f139
expose :source_url do |_|
nil
end
end
end
end

View file

@ -272,17 +272,21 @@ module API
begin
spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::CreateService.new(project: user_project,
current_user: current_user,
params: issue_params,
spam_params: spam_params).execute
result = ::Issues::CreateService.new(project: user_project,
current_user: current_user,
params: issue_params,
spam_params: spam_params).execute
if result.success?
present result[:issue], with: Entities::Issue, current_user: current_user, project: user_project
elsif result[:issue]
issue = result[:issue]
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
with_captcha_check_rest_api(spammable: issue) do
render_validation_error!(issue)
end
else
render_api_error!(result.errors.join(', '), result.http_status || 422)
end
rescue ::ActiveRecord::RecordNotUnique
render_api_error!('Duplicated issue', 409)

View file

@ -36,8 +36,14 @@ module Gitlab
validate_permission!(:create_issue)
result = create_issue
issue = result[:issue]
# issue won't be present only on unrecoverable errors
raise InvalidIssueError, result.errors.join(', ') if result.error? && issue.blank?
verify_record!(
record: create_issue,
record: issue,
invalid_exception: InvalidIssueError,
record_name: 'issue')
end

View file

@ -91,7 +91,7 @@ module Gitlab
end
def create_issue!
@issue = ::Issues::CreateService.new(
result = ::Issues::CreateService.new(
project: project,
current_user: User.support_bot,
params: {
@ -106,7 +106,9 @@ module Gitlab
spam_params: nil
).execute
raise InvalidIssueError unless @issue.persisted?
raise InvalidIssueError if result.error?
@issue = result[:issue]
begin
::Issue::Email.create!(issue: @issue, email_message_id: mail.message_id)

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Gitlab
module Git
# DeclaredLicense is the software license declared in a LICENSE or COPYING
# file in the git repository.
class DeclaredLicense
# SPDX Identifier for the license
attr_reader :key
# Full name of the license
attr_reader :name
# Nickname of the license (optional, a shorter user-friendly name)
attr_reader :nickname
# Filename of the file containing license
attr_accessor :path
# URL that points to the LICENSE
attr_reader :url
def initialize(key: nil, name: nil, nickname: nil, url: nil, path: nil)
@key = key
@name = name
@nickname = nickname
@url = url
@path = path
end
def ==(other)
return unless other.is_a?(self.class)
key == other.key
end
end
end
end

View file

@ -783,10 +783,29 @@ module Gitlab
end
end
def license_short_name
def license(from_gitaly)
wrapped_gitaly_errors do
gitaly_repository_client.license_short_name
response = gitaly_repository_client.find_license
break nil if response.license_short_name.empty?
if from_gitaly
break ::Gitlab::Git::DeclaredLicense.new(key: response.license_short_name,
name: response.license_name,
nickname: response.license_nickname.presence,
url: response.license_url.presence,
path: response.license_path)
end
licensee_object = Licensee::License.new(response.license_short_name)
break nil if licensee_object.name.blank?
licensee_object
end
rescue Licensee::InvalidLicense => e
Gitlab::ErrorTracking.track_exception(e)
nil
end
def fetch_source_branch!(source_repository, source_branch, local_ref)

View file

@ -283,12 +283,10 @@ module Gitlab
response.path.presence
end
def license_short_name
def find_license
request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
response.license_short_name.presence
GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
end
def calculate_checksum

View file

@ -21,12 +21,16 @@ module Gitlab
title = match[:title]
description = match[:description].to_s.rstrip
issue = create_issue(title: title, description: description)
result = create_issue(title: title, description: description)
if issue.persisted?
presenter(issue).present
if result.success?
presenter(result[:issue]).present
elsif result[:issue]
presenter(result[:issue]).display_errors
else
presenter(issue).display_errors
Gitlab::SlashCommands::Presenters::Error.new(
result.errors.join(', ')
).message
end
end

View file

@ -31,9 +31,9 @@ module Quality
}
params[:closed_at] = params[:created_at] + rand(35).days if params[:state] == 'closed'
issue = ::Issues::CreateService.new(project: project, current_user: team.sample, params: params, spam_params: nil).execute_without_rate_limiting
create_result = ::Issues::CreateService.new(project: project, current_user: team.sample, params: params, spam_params: nil).execute_without_rate_limiting
if issue.persisted?
if create_result.success?
created_issues_count += 1
print '.' # rubocop:disable Rails/Output
end

View file

@ -96,7 +96,7 @@ RSpec.describe AutocompleteController do
end
context 'user order' do
it 'shows exact matches first' do
it 'shows exact matches first', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375028' do
reported_user = create(:user, username: 'reported_user', name: 'Doug')
user = create(:user, username: 'user', name: 'User')
user1 = create(:user, username: 'user1', name: 'Ian')

View file

@ -481,6 +481,23 @@ RSpec.describe Boards::IssuesController do
end
end
context 'when create service returns an unrecoverable error' do
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(
ServiceResponse.error(message: 'unrecoverable error', http_status: 404)
)
end
end
it 'returns an array with errors an service http_status' do
create_issue user: user, board: board, list: list1, title: 'New issue'
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to contain_exactly('unrecoverable error')
end
end
context 'with guest user' do
context 'in open list' do
it 'returns a successful 200 response' do

View file

@ -1107,6 +1107,46 @@ RSpec.describe Projects::IssuesController do
end
end
context 'when create service return an unrecoverable error with http_status' do
let(:http_status) { 403 }
before do
allow_next_instance_of(::Issues::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(
ServiceResponse.error(message: 'unrecoverable error', http_status: http_status)
)
end
end
it 'renders 403 and logs the error' do
expect(Gitlab::AppLogger).to receive(:warn).with(
message: 'Cannot create issue',
errors: ['unrecoverable error'],
http_status: http_status
)
post_new_issue
expect(response).to have_gitlab_http_status :forbidden
end
context 'when no render method is found for the returned http_status' do
let(:http_status) { nil }
it 'renders 404 and logs the error' do
expect(Gitlab::AppLogger).to receive(:warn).with(
message: 'Cannot create issue',
errors: ['unrecoverable error'],
http_status: http_status
)
post_new_issue
expect(response).to have_gitlab_http_status :not_found
end
end
end
it 'creates the issue successfully', :aggregate_failures do
issue = post_new_issue

View file

@ -299,14 +299,16 @@ RSpec.describe ProjectsController do
end
it "renders files even with invalid license" do
invalid_license = ::Gitlab::Git::DeclaredLicense.new(key: 'woozle', name: 'woozle wuzzle')
controller.instance_variable_set(:@project, public_project)
expect(public_project.repository).to receive(:license_key).and_return('woozle wuzzle').at_least(:once)
expect(public_project.repository).to receive(:license).and_return(invalid_license).at_least(:once)
get_show
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('_files')
expect(response.body).to have_content('LICENSE') # would be 'MIT license' if stub not works
expect(response.body).to have_content('woozle wuzzle')
end
describe 'tracking events', :snowplow do

View file

@ -2,7 +2,7 @@
FactoryBot.define do
factory :ci_reports_sbom_component, class: '::Gitlab::Ci::Reports::Sbom::Component' do
type { :library }
type { "library" }
sequence(:name) { |n| "component-#{n}" }
sequence(:version) { |n| "v0.0.#{n}" }

View file

@ -219,7 +219,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
end
context 'with a match line' do
it 'does not allow commenting' do
it 'does not allow commenting', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375024' do
match_should_not_allow_commenting(find_by_scrolling('.match', match: :first))
end
end

View file

@ -675,7 +675,7 @@ RSpec.describe 'File blob', :js do
expect(page).to have_content('This project is licensed under the MIT License.')
# shows a learn more link
expect(page).to have_link('Learn more', href: 'http://choosealicense.com/licenses/mit/')
expect(page).to have_link('Learn more', href: 'https://opensource.org/licenses/MIT')
end
end
end

View file

@ -9,7 +9,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_might_not_need_inline do
let(:author) { create(:user) }
let(:project) { create(:project, :public) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } }
let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params, spam_params: nil).execute }
let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params, spam_params: nil).execute[:issue] }
let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }

View file

@ -38,7 +38,7 @@ export function stubComponent(Component, options = {}) {
// Do not render any slots/scoped slots except default
// This differs from VTU behavior which renders all slots
template: '<div><slot></slot></div>',
// allows wrapper.find(Component) to work for stub
// allows wrapper.findComponent(Component) to work for stub
$_vueTestUtils_original: Component,
...options,
};

View file

@ -261,9 +261,14 @@ describe('Pipelines', () => {
);
});
it('tracks tab change click', () => {
it.each(['all', 'finished', 'branches', 'tags'])('tracks %p tab click', async (scope) => {
goToTab(scope);
await waitForPromises();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filter_tabs', {
label: TRACKING_CATEGORIES.tabs,
property: scope,
});
});
});

View file

@ -137,9 +137,11 @@ describe('Deployment component', () => {
if (actionButtons.includes(DeploymentViewButton)) {
it('renders the View button with expected text', () => {
if (status === SUCCESS) {
expect(wrapper.find(DeploymentViewButton).text()).toContain('View app');
expect(wrapper.findComponent(DeploymentViewButton).text()).toContain('View app');
} else {
expect(wrapper.find(DeploymentViewButton).text()).toContain('View latest app');
expect(wrapper.findComponent(DeploymentViewButton).text()).toContain(
'View latest app',
);
}
});
}

View file

@ -825,7 +825,7 @@ RSpec.describe ProjectsHelper do
end
context 'gitaly is working appropriately' do
let(:license) { Licensee::License.new('mit') }
let(:license) { ::Gitlab::Git::DeclaredLicense.new(key: 'mit', name: 'MIT License') }
before do
expect(repository).to receive(:license).and_return(license)

View file

@ -92,6 +92,12 @@ RSpec.describe SessionsHelper do
end
context 'when an email address is very short' do
let(:email) { 'a@b.c' }
it { is_expected.to eq('a@b.c') }
end
context 'when an email address is even shorter' do
let(:email) { 'a@b' }
it { is_expected.to eq('a@b') }

View file

@ -1784,22 +1784,32 @@ RSpec.describe Gitlab::Git::Repository do
end
end
describe '#license_short_name' do
subject { repository.license_short_name }
describe '#license' do
where(from_gitaly: [true, false])
with_them do
subject(:license) { repository.license(from_gitaly) }
context 'when no license file can be found' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
context 'when no license file can be found' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
it { is_expected.to be_nil }
end
it { is_expected.to be_nil }
context 'when an mit license is found' do
it { is_expected.to have_attributes(key: 'mit') }
end
end
context 'when an mit license is found' do
it { is_expected.to eq('mit') }
it 'does not crash when license is not recognized' do
expect(Licensee::License).to receive(:new)
.and_raise(Licensee::InvalidLicense)
expect(repository.license(false)).to be_nil
end
end

View file

@ -53,6 +53,21 @@ RSpec.describe Gitlab::SlashCommands::IssueNew do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to match("- Title is too long")
end
context 'when create issue service return an unrecoverable error' do
let(:regex_match) { described_class.match("issue create title}") }
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'unauthorized'))
end
end
it 'displays the errors' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq('unauthorized')
end
end
end
end

View file

@ -81,7 +81,7 @@ RSpec.describe Integrations::MicrosoftTeams do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute
issue = service.execute[:issue]
service.hook_data(issue, 'open')
end

View file

@ -1331,7 +1331,7 @@ RSpec.describe Repository do
end
end
describe '#license_key', :use_clean_rails_memory_store_caching do
describe '#license_key', :clean_gitlab_redis_cache do
let(:project) { create(:project, :repository) }
before do
@ -1377,48 +1377,46 @@ RSpec.describe Repository do
end
end
describe '#license' do
let(:project) { create(:project, :repository) }
before do
repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
it 'returns nil when no license is detected' do
expect(repository.license).to be_nil
end
it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.license).to be_nil
end
it 'returns nil when license_key is not recognized' do
expect(repository).to receive(:license_key).twice.and_return('not-recognized')
expect(Gitlab::ErrorTracking).to receive(:track_exception) do |ex|
expect(ex).to be_a(Licensee::InvalidLicense)
[true, false].each do |ff|
context "with feature flag license_from_gitaly=#{ff}" do
before do
stub_feature_flags(license_from_gitaly: ff)
end
expect(repository.license).to be_nil
end
describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
let(:project) { create(:project, :repository) }
it 'returns other when the content is not recognizable' do
license = Licensee::License.new('other')
repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
before do
repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
expect(repository.license).to eq(license)
end
it 'returns nil when no license is detected' do
expect(repository.license).to be_nil
end
it 'returns the license' do
license = Licensee::License.new('mit')
repository.create_file(user, 'LICENSE',
license.content,
message: 'Add LICENSE', branch_name: 'master')
it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.license).to eq(license)
expect(repository.license).to be_nil
end
it 'returns other when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq('other')
end
it 'returns the license' do
license = Licensee::License.new('mit')
repository.create_file(user, 'LICENSE',
license.content,
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq(license.key)
end
end
end
end
@ -2207,7 +2205,8 @@ RSpec.describe Repository do
:contribution_guide,
:changelog,
:license_blob,
:license_key,
:license_licensee,
:license_gitaly,
:gitignore,
:gitlab_ci_yml,
:branch_names,
@ -2695,7 +2694,7 @@ RSpec.describe Repository do
match[1].to_sym if match
end.compact
expect(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS).to include(*methods)
expect(Repository::CACHED_METHODS).to include(*methods)
end
end
@ -2860,12 +2859,12 @@ RSpec.describe Repository do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
.with(%i(readme_path license_blob license_key license))
.with(%i(readme_path license_blob license_licensee license_gitaly))
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
expect(repository).to receive(:license)
expect(repository).to receive(:license_licensee)
expect(repository).to receive(:license_gitaly)
repository.refresh_method_caches(%i(readme license))
end

View file

@ -10,13 +10,15 @@ RSpec.describe ProjectPresenter do
describe '#license_short_name' do
context 'when project.repository has a license_key' do
it 'returns the nickname of the license if present' do
allow(project.repository).to receive(:license_key).and_return('agpl-3.0')
allow(project.repository).to receive(:license).and_return(
::Gitlab::Git::DeclaredLicense.new(name: 'foo', nickname: 'GNU AGPLv3'))
expect(presenter.license_short_name).to eq('GNU AGPLv3')
end
it 'returns the name of the license if nickname is not present' do
allow(project.repository).to receive(:license_key).and_return('mit')
allow(project.repository).to receive(:license).and_return(
::Gitlab::Git::DeclaredLicense.new(name: 'MIT License'))
expect(presenter.license_short_name).to eq('MIT License')
end
@ -24,7 +26,7 @@ RSpec.describe ProjectPresenter do
context 'when project.repository has no license_key but a license_blob' do
it 'returns LICENSE' do
allow(project.repository).to receive(:license_key).and_return(nil)
allow(project.repository).to receive(:license).and_return(nil)
expect(presenter.license_short_name).to eq('LICENSE')
end

View file

@ -1164,6 +1164,21 @@ RSpec.describe API::Issues do
expect(json_response['title']).to eq('new issue')
expect(json_response['issue_type']).to eq('issue')
end
context 'when issue create service returns an unrecoverable error' do
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'some error', http_status: 403))
end
end
it 'returns and error message and status code from the service' do
post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('some error')
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid' do

View file

@ -2516,7 +2516,7 @@ RSpec.describe API::Projects do
'name' => project.repository.license.name,
'nickname' => project.repository.license.nickname,
'html_url' => project.repository.license.url,
'source_url' => project.repository.license.meta['source']
'source_url' => nil
})
end

View file

@ -161,7 +161,7 @@ RSpec.describe AlertManagement::CreateAlertIssueService do
it 'has an unsuccessful status' do
expect(execute).to be_error
expect(execute.message).to eq("Title can't be blank")
expect(execute.errors).to contain_exactly("Title can't be blank")
end
end
@ -170,7 +170,7 @@ RSpec.describe AlertManagement::CreateAlertIssueService do
it 'responds with error' do
expect(execute).to be_error
expect(execute.message).to eq('Hosts hosts array is over 255 chars')
expect(execute.errors).to contain_exactly('Hosts hosts array is over 255 chars')
end
end

View file

@ -29,9 +29,10 @@ RSpec.describe Boards::Issues::CreateService do
end
it 'adds the label of the list to the issue' do
issue = service.execute
result = service.execute
expect(issue.labels).to eq [label]
expect(result).to be_success
expect(result[:issue].labels).to contain_exactly(label)
end
end
end

View file

@ -77,7 +77,7 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
it 'responds with errors' do
expect(create_incident).to be_error
expect(create_incident.message).to eq("Title can't be blank")
expect(create_incident.errors).to contain_exactly("Title can't be blank")
end
it 'result payload contains an Issue object' do
@ -98,7 +98,7 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
it 'responds with errors' do
expect(create_incident).to be_error
expect(create_incident.message).to eq('Hosts hosts array is over 255 chars')
expect(create_incident.errors).to contain_exactly('Hosts hosts array is over 255 chars')
end
end
end

View file

@ -63,7 +63,7 @@ RSpec.describe IncidentManagement::PagerDuty::CreateIncidentIssueService do
it 'responds with error' do
expect(execute).to be_error
expect(execute.message).to eq("Title can't be blank")
expect(execute.errors).to contain_exactly("Title can't be blank")
end
end
end

View file

@ -36,6 +36,21 @@ RSpec.describe Issues::CloneService do
context 'issue movable' do
include_context 'user can clone issue'
context 'when issue creation fails' do
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'some error'))
end
end
it 'raises a clone error' do
expect { clone_service.execute(old_issue, new_project) }.to raise_error(
Issues::CloneService::CloneError,
'some error'
)
end
end
context 'generic issue' do
let!(:new_issue) { clone_service.execute(old_issue, new_project, with_notes: with_notes) }

View file

@ -23,12 +23,28 @@ RSpec.describe Issues::CreateService do
let_it_be(:assignee) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
let(:issue) { result[:issue] }
before do
stub_spam_services
end
context 'when params are invalid' do
let(:opts) { { title: '' } }
before_all do
project.add_guest(user)
project.add_guest(assignee)
end
it 'returns an error service response' do
expect(result).to be_error
expect(result.errors).to include("Title can't be blank")
expect(issue).not_to be_persisted
end
end
context 'when params are valid' do
let_it_be(:labels) { create_pair(:label, project: project) }
@ -58,6 +74,7 @@ RSpec.describe Issues::CreateService do
it 'creates the issue with the given params' do
expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
expect(result).to be_success
expect(issue).to be_persisted
expect(issue).to be_a(::Issue)
expect(issue.title).to eq('Awesome issue')
@ -76,12 +93,13 @@ RSpec.describe Issues::CreateService do
end
context 'when a build_service is provided' do
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
let(:issue_from_builder) { WorkItem.new(project: project, title: 'Issue from builder') }
let(:build_service) { double(:build_service, execute: issue_from_builder) }
it 'uses the provided service to build the issue' do
expect(result).to be_success
expect(issue).to be_persisted
expect(issue).to be_a(WorkItem)
end
@ -106,6 +124,7 @@ RSpec.describe Issues::CreateService do
end
it 'sets the correct relative position' do
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.relative_position).to be_present
expect(issue.relative_position).to be_between(issue_before.relative_position, issue_after.relative_position)
@ -183,8 +202,10 @@ RSpec.describe Issues::CreateService do
let_it_be(:non_member) { create(:user) }
it 'filters out params that cannot be set without the :set_issue_metadata permission' do
issue = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute
result = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
expect(issue.description).to eq('please fix')
@ -195,8 +216,10 @@ RSpec.describe Issues::CreateService do
end
it 'can create confidential issues' do
issue = described_class.new(project: project, current_user: non_member, params: { confidential: true }, spam_params: spam_params).execute
result = described_class.new(project: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
expect(issue.confidential).to be_truthy
end
end
@ -391,16 +414,20 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is invalid' do
opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
expect(issue.assignees).to be_empty
end
it 'removes assignee when user id is 0' do
opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
expect(issue.assignees).to be_empty
end
@ -408,8 +435,10 @@ RSpec.describe Issues::CreateService do
project.add_maintainer(assignee)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
expect(issue.assignees).to eq([assignee])
end
@ -426,8 +455,10 @@ RSpec.describe Issues::CreateService do
project.update!(visibility_level: level)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
expect(issue.assignees).to be_empty
end
end
@ -436,7 +467,7 @@ RSpec.describe Issues::CreateService do
end
it_behaves_like 'issuable record that supports quick actions' do
let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute }
let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] }
end
context 'Quick actions' do
@ -459,6 +490,7 @@ RSpec.describe Issues::CreateService do
end
it 'assigns, sets milestone, and sets contact to issuable from command' do
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.assignees).to eq([assignee])
expect(issue.milestone).to eq(milestone)
@ -480,6 +512,8 @@ RSpec.describe Issues::CreateService do
context 'with permission' do
it 'assigns contact to issue' do
group.add_reporter(user)
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.issue_customer_relations_contacts.last.contact).to eq(contact)
end
@ -488,6 +522,8 @@ RSpec.describe Issues::CreateService do
context 'without permission' do
it 'does not assign contact to issue' do
group.add_guest(user)
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.issue_customer_relations_contacts).to be_empty
end
@ -522,6 +558,7 @@ RSpec.describe Issues::CreateService do
end
it 'can apply labels' do
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.labels).to eq([label])
end
@ -556,25 +593,32 @@ RSpec.describe Issues::CreateService do
end
it 'sets default title and description values if not provided' do
issue = described_class.new(
result = described_class.new(
project: project, current_user: user,
params: opts,
spam_params: spam_params
).execute
issue = result[:issue]
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.title).to eq("Follow-up from \"#{merge_request.title}\"")
expect(issue.description).to include("The following discussion from #{merge_request.to_reference} should be addressed")
end
it 'takes params from the request over the default values' do
issue = described_class.new(project: project, current_user: user,
params: opts.merge(
description: 'Custom issue description',
title: 'My new issue'
),
spam_params: spam_params).execute
result = described_class.new(
project: project,
current_user: user,
params: opts.merge(
description: 'Custom issue description',
title: 'My new issue'
),
spam_params: spam_params
).execute
issue = result[:issue]
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.description).to eq('Custom issue description')
expect(issue.title).to eq('My new issue')
@ -600,25 +644,32 @@ RSpec.describe Issues::CreateService do
end
it 'sets default title and description values if not provided' do
issue = described_class.new(
result = described_class.new(
project: project, current_user: user,
params: opts,
spam_params: spam_params
).execute
issue = result[:issue]
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.title).to eq("Follow-up from \"#{merge_request.title}\"")
expect(issue.description).to include("The following discussion from #{merge_request.to_reference} should be addressed")
end
it 'takes params from the request over the default values' do
issue = described_class.new(project: project, current_user: user,
params: opts.merge(
description: 'Custom issue description',
title: 'My new issue'
),
spam_params: spam_params).execute
result = described_class.new(
project: project,
current_user: user,
params: opts.merge(
description: 'Custom issue description',
title: 'My new issue'
),
spam_params: spam_params
).execute
issue = result[:issue]
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.description).to eq('Custom issue description')
expect(issue.title).to eq('My new issue')
@ -635,6 +686,7 @@ RSpec.describe Issues::CreateService do
it 'ignores related issue if not accessible' do
expect { issue }.not_to change { IssueLink.count }
expect(result).to be_success
expect(issue).to be_persisted
end
@ -645,6 +697,7 @@ RSpec.describe Issues::CreateService do
it 'adds a link to the issue' do
expect { issue }.to change { IssueLink.count }.by(1)
expect(result).to be_success
expect(issue).to be_persisted
expect(issue.related_issues(user)).to eq([related_issue])
end

View file

@ -35,6 +35,23 @@ RSpec.describe Issues::MoveService do
let!(:new_issue) { move_service.execute(old_issue, new_project) }
end
context 'when issue creation fails' do
include_context 'user can move issue'
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'some error'))
end
end
it 'raises a move error' do
expect { move_service.execute(old_issue, new_project) }.to raise_error(
Issues::MoveService::MoveError,
'some error'
)
end
end
context 'issue movable' do
include_context 'user can move issue'

View file

@ -166,7 +166,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
let(:opts) { { title: "Awesome issue", description: "please fix" } }
let(:sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute
issue = service.execute[:issue]
service.hook_data(issue, "open")
end