Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-29 18:10:36 +00:00
parent 4d3677a52d
commit 1bbd0179d7
91 changed files with 1323 additions and 309 deletions

View File

@ -328,7 +328,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/app/models/vulnerabilities/finding.rb'
- 'ee/app/presenters/approval_rule_presenter.rb'
- 'ee/app/presenters/ci/minutes/usage_presenter.rb'
- 'ee/app/presenters/merge_request_approver_presenter.rb'
- 'ee/app/serializers/dashboard_operations_project_entity.rb'
- 'ee/app/serializers/ee/member_user_entity.rb'
- 'ee/app/services/app_sec/dast/pipelines/find_latest_service.rb'

View File

@ -1222,7 +1222,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/draft_notes_spec.rb'
- 'spec/requests/api/environments_spec.rb'
- 'spec/requests/api/error_tracking/client_keys_spec.rb'
- 'spec/requests/api/error_tracking/project_settings_spec.rb'
- 'spec/requests/api/files_spec.rb'
- 'spec/requests/api/freeze_periods_spec.rb'
- 'spec/requests/api/go_proxy_spec.rb'

View File

@ -1008,7 +1008,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/models/approval_merge_request_rule_spec.rb'
- 'ee/spec/models/approval_state_spec.rb'
- 'ee/spec/models/approval_wrapped_any_approver_rule_spec.rb'
- 'ee/spec/models/approval_wrapped_code_owner_rule_spec.rb'
- 'ee/spec/models/approval_wrapped_rule_spec.rb'
- 'ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb'
- 'ee/spec/models/approvals/wrapped_rule_set_spec.rb'

View File

@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 16.1.1 (2023-06-28)
### Security (12 changes)
- [Revert 'security-leaked-ci-job-token-permission-16-1' from '16-1'](gitlab-org/security/gitlab@d2599119b120eab983a1446fc9ed3ca801c88368) ([merge request](gitlab-org/security/gitlab!3374))
- [Use fully qualified ref when loading code owner file](gitlab-org/security/gitlab@e8ba90bb85de376bb020350c027bb369671c83d6) ([merge request](gitlab-org/security/gitlab!3356))
- [Maintainer can leak masked webhook secrets by manipulating URL masking](gitlab-org/security/gitlab@2cf91108544e8c30aae6d9b207385c90c299869c) ([merge request](gitlab-org/security/gitlab!3359))
- [Remove approvals when the only commit gets amended](gitlab-org/security/gitlab@3f81f7bc4236bcc2ed887f40b7a14702d756ca9e) ([merge request](gitlab-org/security/gitlab!3366))
- [Add authorization validation to GithubController#failures action](gitlab-org/security/gitlab@3c8c305deef9c9bd1194788b40e0d7ae1de45f3b) ([merge request](gitlab-org/security/gitlab!3335))
- [Fix for fork permissions check in compare controller](gitlab-org/security/gitlab@5b14436f3874de7be62e0f46a25e93a1d8c99975) ([merge request](gitlab-org/security/gitlab!3342))
- [Webhook token leaked in Sidekiq logs if log format is 'default'](gitlab-org/security/gitlab@d2d76399c880c62d7449cdae6014ee3236bffc0b) ([merge request](gitlab-org/security/gitlab!3345))
- [Mitigate epic reference filter ReDOS](gitlab-org/security/gitlab@874d5bc2d55e2e1092bf7cc4ebb0e53fc716d850) ([merge request](gitlab-org/security/gitlab!3341))
- [Increasing security for CI_JOB_TOKEN on public and internal projects](gitlab-org/security/gitlab@c2aa392b932af04e395d67eb06a20b5c768ec683) ([merge request](gitlab-org/security/gitlab!3337))
- [Adjust access to value stream create, edit and destroy actions](gitlab-org/security/gitlab@8a3645e265c71886951bdc03857837aacb57e558) ([merge request](gitlab-org/security/gitlab!3349))
- [Sanitize user email addresses in admin confirm user dialog](gitlab-org/security/gitlab@70553e6ca6b3f244df37e306466e2d3b5d54f76b) ([merge request](gitlab-org/security/gitlab!3338))
- [Obfuscate email of service desk issue creator in issue REST API](gitlab-org/security/gitlab@d0f27b8241ab53bee11f8ce6efb20811690a2d0d) ([merge request](gitlab-org/security/gitlab!3317))
## 16.1.0 (2023-06-21)
### Added (224 changes)
@ -930,6 +947,23 @@ entry.
- [Migrate custom CSS to utility classes](gitlab-org/gitlab@a67999317bec111d523c763fc865665d4ded0aaf) ([merge request](gitlab-org/gitlab!120745)) **GitLab Enterprise Edition**
- [Remove the vsa_group_and_project_parity FF](gitlab-org/gitlab@d090818bdedb0e220928d8e456cf36c8bce81f42) ([merge request](gitlab-org/gitlab!120727)) **GitLab Enterprise Edition**
## 16.0.6 (2023-06-28)
### Security (12 changes)
- [Revert 'security-leaked-ci-job-token-permission-16-0' from '16-0'"](gitlab-org/security/gitlab@3c4fdbad26a123c581253fb501b5bace953a5e85) ([merge request](gitlab-org/security/gitlab!3373))
- [Use fully qualified ref when loading code owner file](gitlab-org/security/gitlab@69c61fcbdc88873b60a217cfd3810364718417e9) ([merge request](gitlab-org/security/gitlab!3355))
- [Maintainer can leak masked webhook secrets by manipulating URL masking](gitlab-org/security/gitlab@a3e055010523db5a1c346464e2589cc75f73629d) ([merge request](gitlab-org/security/gitlab!3360))
- [Remove approvals when the only commit gets amended](gitlab-org/security/gitlab@01e59413e2570744dc34dd50efd2601dc91c8d2d) ([merge request](gitlab-org/security/gitlab!3367))
- [Add authorization validation to GithubController#failures action](gitlab-org/security/gitlab@9eab0689991debab8c8a1afb9e32a3bac9978325) ([merge request](gitlab-org/security/gitlab!3334))
- [Fix for fork permissions check in compare controller](gitlab-org/security/gitlab@da9bb4c761dfe7e8efdd910ed3fc89f348e47e90) ([merge request](gitlab-org/security/gitlab!3343))
- [Webhook token leaked in Sidekiq logs if log format is 'default'](gitlab-org/security/gitlab@a9835cb72eddfae1748c66314618b3157a6bcb57) ([merge request](gitlab-org/security/gitlab!3346))
- [Mitigate epic reference filter ReDOS](gitlab-org/security/gitlab@c8046028a30fe9dca7e141eec2acf3d4b49d93ee) ([merge request](gitlab-org/security/gitlab!3340))
- [Increasing security for CI_JOB_TOKEN on public and internal projects](gitlab-org/security/gitlab@b67db0cdd9324633f4abb59bc27bca43e94e3362) ([merge request](gitlab-org/security/gitlab!3318))
- [Adjust access to value stream create, edit and destroy actions](gitlab-org/security/gitlab@ee20f3f3a84a75c7e07e1aa6fde95761636a669f) ([merge request](gitlab-org/security/gitlab!3321))
- [Sanitize user email addresses in admin confirm user dialog](gitlab-org/security/gitlab@545e0913336e823eb905a8bd86fe2905b321a284) ([merge request](gitlab-org/security/gitlab!3331))
- [Obfuscate email of service desk issue creator in issue REST API](gitlab-org/security/gitlab@b921f10b565bafbd6d50d93d84d34b5f103839ea) ([merge request](gitlab-org/security/gitlab!3315))
## 16.0.5 (2023-06-16)
### Fixed (1 change)
@ -1765,6 +1799,21 @@ entry.
- [Add index to group_group_links table](gitlab-org/gitlab@9a3f2c1a90b54074e61d0abf07101ce664198e81) ([merge request](gitlab-org/gitlab!117386))
- [Validate the projects.creator_id foregin key synchronously](gitlab-org/gitlab@ed9351984a16f20506babf6eab6706b917904ed1) ([merge request](gitlab-org/gitlab!117147))
## 15.11.10 (2023-06-28)
### Security (10 changes)
- [Revert 'security-leaked-ci-job-token-permission-15-11' from '15-11'"](gitlab-org/security/gitlab@19f73bf5494d34b43eb8c807f860d545acae0c32) ([merge request](gitlab-org/security/gitlab!3375))
- [Use fully qualified ref when loading code owner file](gitlab-org/security/gitlab@d7ffb4cca68373bff38bd05f0b8afc868cda9e04) ([merge request](gitlab-org/security/gitlab!3354))
- [Maintainer can leak masked webhook secrets by manipulating URL masking](gitlab-org/security/gitlab@3a7ccdac5e41870fdce362c38d0a1d1437906fbd) ([merge request](gitlab-org/security/gitlab!3361))
- [Remove approvals when the only commit gets amended](gitlab-org/security/gitlab@f8a4ad8be7e5fdf752f525ed58b94b1ce625b9a1) ([merge request](gitlab-org/security/gitlab!3368))
- [Fix for fork permissions check in compare controller](gitlab-org/security/gitlab@8edf44b13e55ffe0c912f98134d0341a5a6bcd28) ([merge request](gitlab-org/security/gitlab!3344))
- [Webhook token leaked in Sidekiq logs if log format is 'default'](gitlab-org/security/gitlab@02b58237085930c62ee277c9ebd89a0560f44a98) ([merge request](gitlab-org/security/gitlab!3347))
- [Mitigate epic reference filter ReDOS](gitlab-org/security/gitlab@4c2cd6e5f7c994aca554be37d9ea9e5e114341f1) ([merge request](gitlab-org/security/gitlab!3339))
- [Increasing security for CI_JOB_TOKEN on public and internal projects](gitlab-org/security/gitlab@4f8a00b2499e876df5b65eca921812fbb3215800) ([merge request](gitlab-org/security/gitlab!3319))
- [Sanitize user email addresses in admin confirm user dialog](gitlab-org/security/gitlab@608c8001c349b0a62aae81850de669d3af02ab60) ([merge request](gitlab-org/security/gitlab!3332))
- [Obfuscate email of service desk issue creator in issue REST API](gitlab-org/security/gitlab@a092ebc54cce4492f87f8ed2bf67c31793b0bd0e) ([merge request](gitlab-org/security/gitlab!3316))
## 15.11.9 (2023-06-15)
### Changed (1 change)

View File

@ -114,8 +114,7 @@ export default {
<template>
<dom-element-listener :selector="$options.FORM_SELECTOR" @[$options.EVENT_SUCCESS]="onSuccess">
<div>
<hr />
<div class="gl-pt-6">
<h5>{{ header }}</h5>
<p v-if="information" data-testid="information-section">

View File

@ -30,26 +30,19 @@ export default {
</script>
<template>
<div class="row">
<div class="col-lg-12">
<hr />
</div>
<div class="col-lg-4">
<h4 class="gl-mt-0"><slot name="title"></slot></h4>
<slot name="description"></slot>
</div>
<div class="col-lg-8">
<input-copy-toggle-visibility
:label="inputLabel"
:label-for="inputId"
:form-input-group-props="formInputGroupProps"
:value="token"
:copy-button-title="copyButtonTitle"
>
<template #description>
<slot name="input-description"></slot>
</template>
</input-copy-toggle-visibility>
</div>
<div>
<h4 class="gl-my-0"><slot name="title"></slot></h4>
<slot name="description"></slot>
<input-copy-toggle-visibility
:label="inputLabel"
:label-for="inputId"
:form-input-group-props="formInputGroupProps"
:value="token"
:copy-button-title="copyButtonTitle"
>
<template #description>
<slot name="input-description"></slot>
</template>
</input-copy-toggle-visibility>
</div>
</template>

View File

@ -91,8 +91,10 @@ export default {
>
<template #title>{{ $options.i18n[tokenType].label }}</template>
<template #description>
<p>{{ $options.i18n[tokenType].description }}</p>
<p>{{ $options.i18n.canNotAccessOtherData }}</p>
<p class="gl-text-secondary">
{{ $options.i18n[tokenType].description }}
{{ $options.i18n.canNotAccessOtherData }}
</p>
</template>
<template #input-description>
<gl-sprintf :message="$options.i18n[tokenType].inputDescription">

View File

@ -57,12 +57,12 @@ async function loadEmojiWithNames() {
}
export async function loadCustomEmojiWithNames() {
if (document.body?.dataset?.group && window.gon?.features?.customEmoji) {
if (document.body?.dataset?.groupFullPath && window.gon?.features?.customEmoji) {
const client = createApolloClient();
const { data } = await client.query({
query: customEmojiQuery,
variables: {
groupPath: document.body.dataset.group,
groupPath: document.body.dataset.groupFullPath,
},
});

View File

@ -74,13 +74,13 @@ export default {
class="gl-display-flex gl-align-items-center gl-mt-2 gl-font-sm"
>
<gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
<span v-safe-html="user.status.message" class="gl-text-truncate"></span>
<span v-safe-html="user.status.message_html" class="gl-text-truncate"></span>
<gl-tooltip
:target="() => $refs.statusTooltipTarget"
boundary="viewport"
placement="bottom"
>
<span v-safe-html="user.status.message"></span>
<span v-safe-html="user.status.message_html"></span>
</gl-tooltip>
</span>
</span>

View File

@ -330,14 +330,6 @@ li.note {
height: 220px;
}
.footer-links {
margin-bottom: 20px;
a {
margin-right: 15px;
}
}
.card.card-body {
margin-bottom: $gl-padding;

View File

@ -62,11 +62,6 @@ body {
}
}
.navless-container {
margin-top: $header-height;
padding-top: $gl-padding * 2;
}
.container-limited {
max-width: $fixed-layout-width;

View File

@ -36,16 +36,6 @@
}
}
// System Footer
.with-system-footer {
// navless pages' footer eg: login page
// navless pages' footer border eg: login page
&.devise-layout-html body .footer-container,
&.devise-layout-html body hr.footer-fixed {
bottom: $system-footer-height;
}
}
.fullscreen-layout {
.header-message,
.footer-message {

View File

@ -228,58 +228,22 @@
}
}
.devise-layout-html {
.html-devise-layout {
margin: 0;
padding: 0;
height: 100%;
&.with-system-header {
.login-page-broadcast {
margin-top: calc(#{$system-header-height} + #{$header-height});
}
}
// Fixes footer container to bottom of viewport
body {
// offset height of fixed header + 1 to avoid scroll
height: calc(100% - 51px);
padding-top: 48px; // Remove this line when the restyle_login_page feature flag is deleted. Instead, add self-align `center` to container, and maybe a top margin.
// offset without the header
&.navless {
height: calc(100% - 11px);
&.with-system-header {
padding-top: $system-header-height;
padding-top: calc(#{$system-header-height} + 48px); // Remove this line when the restyle_login_page feature flag is deleted
}
margin: 0;
padding: 0;
.page-wrap {
min-height: 100%;
position: relative;
}
.footer-container,
hr.footer-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: var(--white, $white);
}
.login-page-broadcast {
margin-top: 40px;
}
.navless-container {
padding: 0 15px 65px; // height of footer + bottom padding of email confirmation link
}
.flash-container {
padding-bottom: 65px;
@include media-breakpoint-down(xs) {
padding-bottom: 0;
&.with-system-footer {
.footer-container {
padding-bottom: $system-footer-height;
}
}
}

View File

@ -111,6 +111,8 @@
}
.wiki-list {
min-height: $gl-spacing-scale-8;
&:hover {
background: $gray-10;

View File

@ -622,10 +622,6 @@ body.navless {
margin-top: 20px;
}
}
.navless-container {
margin-top: var(--header-height, 48px);
padding-top: 32px;
}
.btn {
border-radius: 4px;
font-size: 0.875rem;
@ -685,12 +681,6 @@ hr {
margin: 1.5rem 0;
border-top: 1px solid #ececef;
}
.footer-links {
margin-bottom: 20px;
}
.footer-links a {
margin-right: 15px;
}
.flash-container {
margin: 0;
margin-bottom: 16px;
@ -777,9 +767,15 @@ svg {
.gl-align-items-center {
align-items: center;
}
.gl-flex-wrap {
flex-wrap: wrap;
}
.gl-justify-content-space-between {
justify-content: space-between;
}
.gl-align-self-end {
align-self: flex-end;
}
.gl-w-10 {
width: 3.5rem;
}
@ -794,6 +790,9 @@ svg {
width: 100%;
}
}
.gl-h-full {
height: 100%;
}
.gl-p-5 {
padding: 1rem;
}
@ -805,6 +804,9 @@ svg {
padding-top: 1rem;
padding-bottom: 1rem;
}
.gl-m-0 {
margin: 0;
}
.gl-mt-3 {
margin-top: 0.5rem;
}
@ -823,6 +825,9 @@ svg {
.gl-ml-auto {
margin-left: auto;
}
.gl-gap-5 {
gap: 1rem;
}
@media (min-width: 576px) {
.gl-sm-mt-0 {
margin-top: 0;

View File

@ -7,6 +7,9 @@ module Analytics
included do
before_action :authorize
# Defining the before action here, because in the EE module we cannot define a before_action.
# Reason: this is a module which is being included into a controller. This module is extended in EE.
before_action :authorize_modification, only: %i[create destroy update] # rubocop:disable Rails/LexicallyScopedActionFilter
end
def index
@ -25,6 +28,10 @@ module Analytics
def authorize
authorize_read_cycle_analytics!
end
def authorize_modification
# no-op, overridden in EE
end
end
end
end

View File

@ -7,6 +7,8 @@ class Import::GithubController < Import::BaseController
include ActionView::Helpers::SanitizeHelper
include Import::GithubOauth
before_action :authorize_owner_access!, except: [:new, :callback, :personal_access_token, :status, :details, :create,
:realtime_changes, :cancel_all, :counts]
before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :realtime_changes, :create]
before_action :expire_etag_cache, only: [:status, :create]
@ -92,8 +94,6 @@ class Import::GithubController < Import::BaseController
end
def failures
project = Project.imported_from(provider_name).find(params[:project_id])
unless project.import_finished?
return render status: :bad_request, json: {
message: _('The import is not complete.')
@ -107,7 +107,6 @@ class Import::GithubController < Import::BaseController
end
def cancel
project = Project.imported_from(provider_name).find(params[:project_id])
result = Import::Github::CancelProjectImportService.new(project, current_user).execute
if result[:status] == :success
@ -168,6 +167,14 @@ class Import::GithubController < Import::BaseController
private
def project
@project ||= Project.imported_from(provider_name).find(params[:project_id])
end
def authorize_owner_access!
return render_404 unless current_user.can?(:owner_access, project)
end
def import_params
params.permit(permitted_import_params)
end

View File

@ -89,10 +89,14 @@ class Projects::CompareController < Projects::ApplicationController
# target == start_ref == from
def target_project
strong_memoize(:target_project) do
next source_project.default_merge_request_target unless compare_params.key?(:from_project_id)
next source_project if compare_params[:from_project_id].to_i == source_project.id
target_project = target_projects(source_project).find_by_id(compare_params[:from_project_id])
target_project =
if !compare_params.key?(:from_project_id)
source_project.default_merge_request_target
elsif compare_params[:from_project_id].to_i == source_project.id
source_project
else
target_projects(source_project).find_by_id(compare_params[:from_project_id])
end
# Just ignore the field if it points at a non-existent or hidden project
next source_project unless target_project && can?(current_user, :read_code, target_project)

View File

@ -727,6 +727,8 @@ module Types
if minimum_access_level.nil?
object.forks.public_or_visible_to_user(current_user)
else
return [] if current_user.nil?
object.forks.visible_to_user_and_access_level(current_user, minimum_access_level)
end
end

View File

@ -124,7 +124,8 @@ module ApplicationHelper
page: body_data_page,
page_type_id: controller.params[:id],
find_file: find_file_path,
group: @group&.path
group: @group&.path,
group_full_path: @group&.full_path
}.merge(project_data)
end
@ -135,6 +136,7 @@ module ApplicationHelper
project_id: @project.id,
project: @project.path,
group: @project.group&.path,
group_full_path: @project.group&.full_path,
namespace_id: @project.namespace&.id
}
end
@ -328,7 +330,7 @@ module ApplicationHelper
class_names << 'with-system-header' if appearance.show_header?
class_names << 'with-system-footer' if appearance.show_footer?
class_names
class_names.join(' ')
end
# Returns active css class when condition returns true

View File

@ -152,7 +152,8 @@ module SidebarsHelper
customized: user.status&.customized?,
availability: user.status&.availability.to_s,
emoji: user.status&.emoji,
message: user.status&.message_html&.html_safe,
message_html: user.status&.message_html&.html_safe,
message: user.status&.message,
clear_after: user_clear_status_at(user)
}
end

View File

@ -136,7 +136,7 @@ module UsersHelper
def confirm_user_data(user)
message = if user.unconfirmed_email.present?
_('This user has an unconfirmed email address (%{email}). You may force a confirmation.') % { email: user.unconfirmed_email }
safe_format(_('This user has an unconfirmed email address (%{email}). You may force a confirmation.'), email: user.unconfirmed_email)
else
_('This user has an unconfirmed email address. You may force a confirmation.')
end

View File

@ -135,6 +135,7 @@ class WebHook < ApplicationRecord
return if url_variables_were.blank? || interpolated_url_was == interpolated_url
self.url_variables = {} if url_variables_were.keys.intersection(url_variables.keys).any?
self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any?
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module SystemAccess
def self.table_name_prefix
'system_access_'
end
end

View File

@ -8,7 +8,7 @@ module ImportCsv
@user = user
@project = project
@csv_io = csv_io
@results = { success: 0, error_lines: [], parse_error: false }
@results = { success: 0, error_lines: [], parse_error: false, preprocess_errors: {} }
end
PreprocessError = Class.new(StandardError)

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module ImportCsv
class PreprocessMilestonesService < BaseService
def initialize(user, project, provided_titles)
@user = user
@project = project
@provided_titles = provided_titles
@results = { success: 0, errors: nil }
@milestone_errors = { missing: { header: {}, titles: [] } }
end
attr_reader :user, :project, :provided_titles, :results, :milestone_errors
def execute
available_milestones = find_milestones_by_titles
return ServiceResponse.success if provided_titles.sort == available_milestones.sort
milestone_errors[:missing][:header] = 'Milestone'
milestone_errors[:missing][:titles] = provided_titles.difference(available_milestones) || []
ServiceResponse.error(message: "", payload: milestone_errors)
end
def find_milestones_by_titles
# Find if these milestones exist in the project or its group and group ancestors
finder_params = {
project_ids: [project.id],
title: provided_titles
}
finder_params[:group_ids] = project.group.self_and_ancestors.select(:id) if project.group
MilestonesFinder.new(finder_params).execute.map(&:title).uniq
end
end
end

View File

@ -23,6 +23,24 @@ module Issuable
raise CSV::MalformedCSVError.new('Invalid CSV format - missing required headers.', 1)
end
def preprocess!
preprocess_milestones!
raise PreprocessError if results[:preprocess_errors].any?
end
def preprocess_milestones!
# Pre-Process Milestone if header is present
return unless csv_data.lines.first.downcase.include?('milestone')
provided_titles = with_csv_lines.filter_map { |row| row[:milestone]&.strip&.downcase }.uniq
result = ::ImportCsv::PreprocessMilestonesService.new(user, project, provided_titles).execute
return if result.success?
# collate errors here and throw errors
results[:preprocess_errors][:milestone_errors] = result.payload
end
end
end
end

View File

@ -1,10 +1,11 @@
%hr.footer-fixed
.container.footer-container.gl-display-flex.gl-justify-content-space-between
.footer-links
- unless public_visibility_restricted?
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
= link_to _("Community forum"), ApplicationHelper.community_forum, target: '_blank', class: 'text-nowrap', rel: 'noopener noreferrer'
= render 'devise/shared/language_switcher'
.footer-container.gl-w-full.gl-align-self-end
%hr.gl-m-0
.container.gl-py-5.gl-display-flex.gl-justify-content-space-between
.gl-display-flex.gl-gap-5.gl-flex-wrap
- unless public_visibility_restricted?
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
= link_to _("Community forum"), ApplicationHelper.community_forum, target: '_blank', class: 'text-nowrap', rel: 'noopener noreferrer'
= render 'devise/shared/language_switcher'
= footer_message

View File

@ -1,14 +1,14 @@
- add_page_specific_style 'page_bundles/login'
- custom_text = custom_sign_in_description
!!! 5
%html.devise-layout-html{ class: system_message_class }
%html.html-devise-layout{ lang: "en" }
= render "layouts/head", { startup_filename: 'signin' }
%body.login-page.application.navless{ class: "#{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
%body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
= header_message
= render "layouts/init_client_detection_flags"
- if Feature.enabled?(:restyle_login_page, @project)
.page-wrap.borderless
.container.navless-container
.gl-h-full.borderless.gl-display-flex.gl-flex-wrap
.container
.content
= render "layouts/flash"
- if custom_text.present?
@ -33,11 +33,11 @@
.gl-w-half.gl-xs-w-full.gl-ml-auto.gl-mr-auto.bar
= yield
= render 'devise/shared/footer', footer_message: footer_message
= render 'devise/shared/footer'
- else
.page-wrap
= render "layouts/header/empty"
.container.navless-container
= render "layouts/header/empty"
.gl-h-full.gl-display-flex.gl-flex-wrap
.container
.content
= render "layouts/flash"
.row.mt-3
@ -63,4 +63,4 @@
.col-md-6.order-1.new-session-forms-container{ class: recently_confirmed_com? ? 'order-sm-first' : 'order-sm-12' }
= yield
= render 'devise/shared/footer', footer_message: footer_message
= render 'devise/shared/footer'

View File

@ -1,15 +1,15 @@
- add_page_specific_style 'page_bundles/login'
!!! 5
%html.devise-layout-html{ lang: "en", class: system_message_class }
%html.html-devise-layout{ lang: "en" }
= render "layouts/head"
%body.login-page.application.navless{ class: "#{user_application_theme} #{client_class_list}" }
%body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}" }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
.content
= render "layouts/flash"
= yield
.gl-h-full.gl-display-flex.gl-flex-wrap
.container
.content
= render "layouts/flash"
= yield
= render 'devise/shared/footer', footer_message: footer_message
= render 'devise/shared/footer'

View File

@ -1,13 +1,14 @@
- add_page_specific_style 'page_bundles/signup'
- add_page_specific_style 'page_bundles/login'
!!! 5
%html.devise-layout-html.navless{ class: system_message_class }
- add_page_specific_style 'page_bundles/signup'
- add_page_specific_style 'page_bundles/login'
%html.html-devise-layout{ lang: 'en' }
= render "layouts/head"
%body.signup-page{ class: "#{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
= render "layouts/header/logo_with_title"
%body.signup-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
= header_message
= render "layouts/init_client_detection_flags"
.page-wrap
.container.signup-box-container.navless-container
= render "layouts/broadcast"
.content
= yield
= render "layouts/header/logo_with_title"
.container
.content
= yield
= footer_message

View File

@ -1,4 +1,5 @@
- text_style = 'font-size:16px; text-align:center; line-height:30px;'
- error_style = 'font-size:13px; text-align:center; line-height:16px; color:#dd2b0e;'
%p{ style: text_style }
- project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none;")
@ -16,3 +17,18 @@
- if @results[:parse_error]
%p{ style: text_style }
= s_('Notify|Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values.')
- preprocess_errors = @results[:preprocess_errors]
- if preprocess_errors.present?
- missing_milestone_errors = preprocess_errors.dig(:milestone_errors, :missing) || []
- if missing_milestone_errors.present?
%p{ style: error_style }
= s_('Notify|Could not find the following %{column} values in %{project}%{parent_groups_clause}: %{error_lines}') % { error_lines: missing_milestone_errors[:titles].join(', '),
column: missing_milestone_errors[:header].downcase, project: @project.full_name,
parent_groups_clause: @project.group.present? ? ' or its parent groups' : ''}
- if @results[:error_lines].present? || preprocess_errors.present?
%p{ style: text_style }
= s_('Notify|Please fix the errors above and try the CSV import again.')

View File

@ -9,3 +9,20 @@ Errors found on line <%= 'number'.pluralize(@results[:error_lines].size) %>: <%=
<% if @results[:parse_error] %>
Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values.
<% end %>
<% preprocess_errors = @results[:preprocess_errors] %>
<%
if preprocess_errors.present?
missing_milestone_errors = preprocess_errors.dig(:milestone_errors, :missing) || []
%>
<% if missing_milestone_errors.present? %>
<%= s_('Notify|Could not find the following %{column} values in %{project}%{parent_groups_clause}: %{error_lines}') % { error_lines: missing_milestone_errors[:titles].join(', '),
column: missing_milestone_errors[:header].downcase, project: @project.full_name,
parent_groups_clause: @project.group.present? ? ' or its parent groups' : ''} %>
<% end %>
<% end %>
<% if @results[:error_lines].present? || preprocess_errors.present? %>
<%= s_('Notify|Please fix the errors above and try the CSV import again.') %>
<% end %>

View File

@ -4,27 +4,24 @@
- type_plural = _('personal access tokens')
- @force_desktop_expanded_sidebar = true
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
%p
= s_('AccessTokens|You can generate a personal access token for each application you use that needs access to the GitLab API.')
%p
= s_('AccessTokens|You can also use personal access tokens to authenticate against Git over HTTP.')
= s_('AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.')
.gl-pb-6.js-search-settings-section
%h4.gl-my-0
= page_title
%p.gl-text-secondary
= s_('AccessTokens|You can generate a personal access token for each application you use that needs access to the GitLab API.')
= s_('AccessTokens|You can also use personal access tokens to authenticate against Git over HTTP.')
= s_('AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.')
.col-lg-8
#js-new-access-token-app{ data: { access_token_type: type } }
#js-new-access-token-app{ data: { access_token_type: type } }
= render 'shared/access_tokens/form',
ajax: true,
type: type,
path: profile_personal_access_tokens_path,
token: @personal_access_token,
scopes: @scopes,
help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
= render 'shared/access_tokens/form',
ajax: true,
type: type,
path: profile_personal_access_tokens_path,
token: @personal_access_token,
scopes: @scopes,
help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } }
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } }
#js-tokens-app{ data: { tokens_data: tokens_app_data } }

View File

@ -9,34 +9,25 @@
%h5.gl-mt-0
= title
%p.profile-settings-content
= s_("AccessTokens|Enter the name of your application, and we'll return a unique %{type}.") % { type: type }
= gitlab_ui_form_for token, as: prefix, url: path, method: :post, html: { id: 'js-new-access-token-form', class: 'js-requires-input' }, remote: ajax do |f|
= form_errors(token)
.row
.form-group.col
.row
= f.label :name, s_('AccessTokens|Token name'), class: 'label-bold col-md-12'
.col-md-6
- resource_type = resource.is_a?(Group) ? "group" : "project"
= f.text_field :name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'access_token_name_field' }, :'aria-describedby' => 'access_token_help_text'
%span.form-text.text-muted.col-md-12#access_token_help_text= s_("AccessTokens|For example, the application using the token or the purpose of the token. Do not give sensitive information for the name of the token, as it will be visible to all %{resource_type} members.") % { resource_type: resource_type }
.form-group
= f.label :name, s_('AccessTokens|Token name'), class: 'label-bold'
- resource_type = resource.is_a?(Group) ? "group" : "project"
= f.text_field :name, class: 'form-control gl-form-input gl-max-w-80', required: true, data: { qa_selector: 'access_token_name_field' }, :'aria-describedby' => 'access_token_help_text'
%span.form-text.text-muted#access_token_help_text= s_("AccessTokens|For example, the application using the token or the purpose of the token. Do not give sensitive information for the name of the token, as it will be visible to all %{resource_type} members.") % { resource_type: resource_type }
.row
.col
.js-access-tokens-expires-at{ data: expires_at_field_data }
= f.text_field :expires_at, class: 'gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
.js-access-tokens-expires-at{ data: expires_at_field_data }
= f.text_field :expires_at, class: 'gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
- if resource
.row
.form-group.col-md-6
= label_tag :access_level, s_("AccessTokens|Select a role"), class: "label-bold"
.select-wrapper
= select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control", data: { qa_selector: 'access_token_access_level' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
= label_tag :access_level, s_("AccessTokens|Select a role"), class: "label-bold"
.select-wrapper
= select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control", data: { qa_selector: 'access_token_access_level' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
%b{ :'aria-describedby' => 'select_scope_help_text' }

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_security_dast_on_demand_api_scan_monthly
name: "dast_on_demand_api_scan"
description: Count of pipelines using the latest DAST API template
product_section: sec
product_stage: secure
product_group: "dynamic_analysis"
value_type: number
status: active
milestone: "14.7"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73564
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- p_ci_templates_security_dast_on_demand_api_scan
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_security_dast_on_demand_api_scan_weekly
name: "dast_on_demand_api_scan"
description: Count of pipelines using the latest DAST API template
product_section: sec
product_stage: secure
product_group: "dynamic_analysis"
value_type: number
status: active
milestone: "14.7"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73564
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- p_ci_templates_security_dast_on_demand_api_scan
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,10 @@
---
table_name: system_access_microsoft_applications
classes:
- SystemAccess::MicrosoftApplication
feature_categories:
- system_access
description: Integration with Microsoft Azure application
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124101
milestone: '16.2'
gitlab_schema: gitlab_main

View File

@ -0,0 +1,10 @@
---
table_name: system_access_microsoft_graph_access_tokens
classes:
- SystemAccess::MicrosoftGraphAccessToken
feature_categories:
- system_access
description: Access tokens for the Microsoft Graph API
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124101
milestone: '16.2'
gitlab_schema: gitlab_main

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class CreateSystemAccessMicrosoftApplication < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def change
create_table :system_access_microsoft_applications do |t|
t.timestamps_with_timezone null: false
t.references :namespace, index: { unique: true }, foreign_key: { on_delete: :cascade }
t.boolean :enabled, null: false, default: false
t.text :tenant_xid, null: false, limit: 255
t.text :client_xid, null: false, limit: 255
t.text :login_endpoint, null: false, limit: 255, default: 'https://login.microsoftonline.com'
t.text :graph_endpoint, null: false, limit: 255, default: 'https://graph.microsoft.com'
t.binary :encrypted_client_secret, null: false
t.binary :encrypted_client_secret_iv, null: false
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateSystemAccessMicrosoftGraphAccessTokens < Gitlab::Database::Migration[2.1]
def change
create_table :system_access_microsoft_graph_access_tokens do |t|
t.timestamps_with_timezone null: false
t.references :system_access_microsoft_application,
index: { name: 'unique_index_sysaccess_ms_access_tokens_on_sysaccess_ms_app_id', unique: true },
foreign_key: { on_delete: :cascade }
t.integer :expires_in, null: false
t.binary :encrypted_token, null: false
t.binary :encrypted_token_iv, null: false
end
end
end

View File

@ -0,0 +1 @@
de2c254df58e13ffba7fef9bbf4fff2e244aa46ce58f8245646ed7ce4ab51770

View File

@ -0,0 +1 @@
29cf1dfb1429cb177f5b6cb2fae2a0bc388c0c6cbda5c4405456afcee8374a54

View File

@ -23116,6 +23116,52 @@ CREATE SEQUENCE suggestions_id_seq
ALTER SEQUENCE suggestions_id_seq OWNED BY suggestions.id;
CREATE TABLE system_access_microsoft_applications (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
namespace_id bigint,
enabled boolean DEFAULT false NOT NULL,
tenant_xid text NOT NULL,
client_xid text NOT NULL,
login_endpoint text DEFAULT 'https://login.microsoftonline.com'::text NOT NULL,
graph_endpoint text DEFAULT 'https://graph.microsoft.com'::text NOT NULL,
encrypted_client_secret bytea NOT NULL,
encrypted_client_secret_iv bytea NOT NULL,
CONSTRAINT check_042f6b21aa CHECK ((char_length(login_endpoint) <= 255)),
CONSTRAINT check_1e8b2d405f CHECK ((char_length(tenant_xid) <= 255)),
CONSTRAINT check_339c3ffca8 CHECK ((char_length(graph_endpoint) <= 255)),
CONSTRAINT check_ee72fb5459 CHECK ((char_length(client_xid) <= 255))
);
CREATE SEQUENCE system_access_microsoft_applications_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE system_access_microsoft_applications_id_seq OWNED BY system_access_microsoft_applications.id;
CREATE TABLE system_access_microsoft_graph_access_tokens (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
system_access_microsoft_application_id bigint,
expires_in integer NOT NULL,
encrypted_token bytea NOT NULL,
encrypted_token_iv bytea NOT NULL
);
CREATE SEQUENCE system_access_microsoft_graph_access_tokens_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE system_access_microsoft_graph_access_tokens_id_seq OWNED BY system_access_microsoft_graph_access_tokens.id;
CREATE TABLE system_note_metadata (
id integer NOT NULL,
commit_count integer,
@ -25885,6 +25931,10 @@ ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscription
ALTER TABLE ONLY suggestions ALTER COLUMN id SET DEFAULT nextval('suggestions_id_seq'::regclass);
ALTER TABLE ONLY system_access_microsoft_applications ALTER COLUMN id SET DEFAULT nextval('system_access_microsoft_applications_id_seq'::regclass);
ALTER TABLE ONLY system_access_microsoft_graph_access_tokens ALTER COLUMN id SET DEFAULT nextval('system_access_microsoft_graph_access_tokens_id_seq'::regclass);
ALTER TABLE ONLY system_note_metadata ALTER COLUMN id SET DEFAULT nextval('system_note_metadata_id_seq'::regclass);
ALTER TABLE ONLY taggings ALTER COLUMN id SET DEFAULT nextval('taggings_id_seq'::regclass);
@ -28379,6 +28429,12 @@ ALTER TABLE ONLY subscriptions
ALTER TABLE ONLY suggestions
ADD CONSTRAINT suggestions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY system_access_microsoft_applications
ADD CONSTRAINT system_access_microsoft_applications_pkey PRIMARY KEY (id);
ALTER TABLE ONLY system_access_microsoft_graph_access_tokens
ADD CONSTRAINT system_access_microsoft_graph_access_tokens_pkey PRIMARY KEY (id);
ALTER TABLE ONLY system_note_metadata
ADD CONSTRAINT system_note_metadata_pkey PRIMARY KEY (id);
@ -32950,6 +33006,8 @@ CREATE INDEX index_successful_deployments_on_cluster_id_and_environment_id ON de
CREATE UNIQUE INDEX index_suggestions_on_note_id_and_relative_order ON suggestions USING btree (note_id, relative_order);
CREATE UNIQUE INDEX index_system_access_microsoft_applications_on_namespace_id ON system_access_microsoft_applications USING btree (namespace_id);
CREATE UNIQUE INDEX index_system_note_metadata_on_description_version_id ON system_note_metadata USING btree (description_version_id) WHERE (description_version_id IS NOT NULL);
CREATE UNIQUE INDEX index_system_note_metadata_on_note_id ON system_note_metadata USING btree (note_id);
@ -33620,6 +33678,8 @@ CREATE UNIQUE INDEX unique_index_for_project_pages_unique_domain ON project_sett
CREATE UNIQUE INDEX unique_index_on_system_note_metadata_id ON resource_link_events USING btree (system_note_metadata_id);
CREATE UNIQUE INDEX unique_index_sysaccess_ms_access_tokens_on_sysaccess_ms_app_id ON system_access_microsoft_graph_access_tokens USING btree (system_access_microsoft_application_id);
CREATE UNIQUE INDEX unique_instance_audit_event_destination_name ON audit_events_instance_external_audit_event_destinations USING btree (name);
CREATE UNIQUE INDEX unique_merge_request_diff_llm_summaries_on_mr_diff_id ON merge_request_diff_llm_summaries USING btree (merge_request_diff_id);
@ -36917,6 +36977,9 @@ ALTER TABLE ONLY incident_management_oncall_participants
ALTER TABLE ONLY work_item_parent_links
ADD CONSTRAINT fk_rails_601d5bec3a FOREIGN KEY (work_item_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY system_access_microsoft_graph_access_tokens
ADD CONSTRAINT fk_rails_604908851f FOREIGN KEY (system_access_microsoft_application_id) REFERENCES system_access_microsoft_applications(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_state_transitions
ADD CONSTRAINT fk_rails_60e4899648 FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
@ -37607,6 +37670,9 @@ ALTER TABLE ONLY ci_job_artifacts
ALTER TABLE ONLY organization_settings
ADD CONSTRAINT fk_rails_c56e4690c0 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY system_access_microsoft_applications
ADD CONSTRAINT fk_rails_c5b7765d04 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_settings
ADD CONSTRAINT fk_rails_c6df6e6328 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -481,7 +481,7 @@ Streaming destination is deleted if:
- The returned `errors` object is empty.
- The API responds with `200 OK`.
To remove an HTTP header, use the GraphQL `auditEventsStreamingInstanceHeadersDestroy` mutation.
To remove an HTTP header, use the GraphQL `auditEventsStreamingInstanceHeadersDestroy` mutation.
To retrieve the header ID,
[list all the custom HTTP headers](#list-streaming-destinations) for the instance.

View File

@ -97,7 +97,7 @@ POST groups/:id/access_tokens
| `name` | String | yes | Name of the group access token |
| `scopes` | `Array[String]` | yes | [List of scopes](../user/group/settings/group_access_tokens.md#scopes-for-a-group-access-token) |
| `access_level` | Integer | no | Access level. Valid values are `10` (Guest), `20` (Reporter), `30` (Developer), `40` (Maintainer), and `50` (Owner). |
| `expires_at` | Date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If no date is set, the expiration is set to the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#when-personal-access-tokens-expire). |
| `expires_at` | Date | yes | Expiration date of the access token in ISO format (`YYYY-MM-DD`). The date cannot be set later than the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#when-personal-access-tokens-expire). |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \

View File

@ -106,7 +106,7 @@ POST projects/:id/access_tokens
| `name` | String | yes | Name of the project access token |
| `scopes` | `Array[String]` | yes | [List of scopes](../user/project/settings/project_access_tokens.md#scopes-for-a-project-access-token) |
| `access_level` | Integer | no | Access level. Valid values are `10` (Guest), `20` (Reporter), `30` (Developer), `40` (Maintainer), and `50` (Owner). Defaults to `40`. |
| `expires_at` | Date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If no date is set, the expiration is set to the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#when-personal-access-tokens-expire). |
| `expires_at` | Date | yes | Expiration date of the access token in ISO format (`YYYY-MM-DD`). The date cannot be set later than the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#when-personal-access-tokens-expire). |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \

View File

@ -578,6 +578,248 @@ GET /users/:user_id/projects
]
```
## List projects a user has contributed to
Get a list of visible projects a given user has contributed to.
```plaintext
GET /users/:user_id/contributed_projects
```
| Attribute | Type | Required | Description |
|-------------------------------|---------|------------------------|-------------|
| `user_id` | string | **{check-circle}** Yes | The ID or username of the user. |
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. Without authentication, this operation is a no-op; only simple fields are returned. |
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/5/contributed_projects"
```
Example response:
```json
[
{
"id": 4,
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"description_html": "<p data-sourcepos=\"1:1-1:56\" dir=\"auto\">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
"default_branch": "master",
"visibility": "private",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
"readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
"tag_list": [ //deprecated, use `topics` instead
"example",
"disapora client"
],
"topics": [
"example",
"disapora client"
],
"owner": {
"id": 3,
"name": "Diaspora",
"created_at": "2013-09-30T13:46:02Z"
},
"name": "Diaspora Client",
"name_with_namespace": "Diaspora / Diaspora Client",
"path": "diaspora-client",
"path_with_namespace": "diaspora/diaspora-client",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
"container_registry_enabled": false, // deprecated, use container_registry_access_level instead
"container_registry_access_level": "disabled",
"security_and_compliance_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"updated_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
"id": 3,
"name": "Diaspora",
"path": "diaspora",
"kind": "group",
"full_path": "diaspora"
},
"import_status": "none",
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
"shared_runners_enabled": true,
"group_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
"restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"squash_option": "default_on",
"autoclose_referenced_issues": true,
"enforce_auth_checks_on_uploads": true,
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
"merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
"repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
"labels": "http://example.com/api/v4/projects/1/labels",
"events": "http://example.com/api/v4/projects/1/events",
"members": "http://example.com/api/v4/projects/1/members",
"cluster_agents": "http://example.com/api/v4/projects/1/cluster_agents"
}
},
{
"id": 6,
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"description_html": "<p data-sourcepos=\"1:1-1:56\" dir=\"auto\">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>",
"default_branch": "master",
"visibility": "private",
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
"readme_url": "http://example.com/brightbox/puppet/blob/master/README.md",
"tag_list": [ //deprecated, use `topics` instead
"example",
"puppet"
],
"topics": [
"example",
"puppet"
],
"owner": {
"id": 4,
"name": "Brightbox",
"created_at": "2013-09-30T13:46:02Z"
},
"name": "Puppet",
"name_with_namespace": "Brightbox / Puppet",
"path": "puppet",
"path_with_namespace": "brightbox/puppet",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
"container_registry_enabled": false, // deprecated, use container_registry_access_level instead
"container_registry_access_level": "disabled",
"security_and_compliance_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"updated_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
"id": 4,
"name": "Brightbox",
"path": "brightbox",
"kind": "group",
"full_path": "brightbox"
},
"import_status": "none",
"import_error": null,
"permissions": {
"project_access": {
"access_level": 10,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
},
"archived": false,
"avatar_url": null,
"shared_runners_enabled": true,
"group_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
"restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"squash_option": "default_on",
"auto_devops_enabled": true,
"auto_devops_deploy_strategy": "continuous",
"repository_storage": "default",
"approvals_before_merge": 0, // Deprecated. Use merge request approvals API instead.
"mirror": false,
"mirror_user_id": 45,
"mirror_trigger_builds": false,
"only_mirror_protected_branches": false,
"mirror_overwrites_diverged_branches": false,
"external_authorization_classification_label": null,
"packages_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
"enforce_auth_checks_on_uploads": true,
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
"merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
"repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
"labels": "http://example.com/api/v4/projects/1/labels",
"events": "http://example.com/api/v4/projects/1/events",
"members": "http://example.com/api/v4/projects/1/members",
"cluster_agents": "http://example.com/api/v4/projects/1/cluster_agents"
}
}
]
```
## List projects starred by a user
> The `_links.cluster_agents` attribute in the response [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/347047) in GitLab 14.10.

View File

@ -56,7 +56,7 @@ of the container registry for both GitLab.com and for self-managed users.
## Proposal
There are two main components that must be further developed in order for
self-managed admins to move to the registry database: the deployment environment and
self-managed admins to move to the registry database: the deployment environment and
the registry migration tooling.
For the deployment environments need to document what the user needs to do to set up their
@ -108,7 +108,7 @@ methods.
Given that we're not mutating data via object storage as part of the import
process, we should not need to double-check these drivers or try to predict
potential errors. Relying on user feedback during the beta to direct any efforts
we should be making here could prevent us from scheduling unnecessary work.
we should be making here could prevent us from scheduling unnecessary work.
#### Arguments in Favor of Structuring Support by Driver
@ -154,7 +154,7 @@ the surrounding process will enable non-expert users to import their registries
with both minimal risk and with minimal support from GitLab team members.
Therefore, the most important work remaining is crafting the UX of this tooling
such that those goals are met. This
[epic](https://gitlab.com/groups/gitlab-org/-/epics/8602) captures many of the
[epic](https://gitlab.com/groups/gitlab-org/-/epics/8602) captures many of the
proposed improvements.
#### Design
@ -186,7 +186,7 @@ migration, importing tags requires that the registry be offline or in
read-only mode. This step does the minimum possible work to achieve fast and
efficient tag imports and will always be the fastest of the three steps, reducing
the downtime component to a fraction of the total import time. The user can then
bring up the registry configured to use the metadata database. After that, the
bring up the registry configured to use the metadata database. After that, the
user is free to run the third step during normal registry operations. This step
makes any dangling blobs in common storage visible to the database and therefore
the online garbage collection process.

View File

@ -156,7 +156,7 @@ The new UI will be built using the Pajamas Design System in accordance with GitL
- provision API
- remove existing iframe provisioning
- UI for trace detail
- UI for filtering/searching traces
- UI for filtering/searching traces
- basic e2e test for provision, send data, query in UI
- metrics, dashboards, alerts

View File

@ -167,7 +167,7 @@ job1:
The order of caches extraction is:
1. Retrieval attempt for `cache:key`
1. Retrieval attemps for each entry in order in `fallback_keys`
1. Retrieval attempts for each entry in order in `fallback_keys`
1. Retrieval attempt for the global fallback key in `CACHE_FALLBACK_KEY`
The cache extraction process stops after the first successful cache is retrieved.

View File

@ -23,9 +23,10 @@ Configure a dashboard to use it for a given environment.
You can configure dashboard for an environment that already exists, or
add one when you create an environment.
Prerequisite:
Prerequisites:
- The agent for Kubernetes must be shared with the environment's project, or its parent group, using the [`user_access`](../../user/clusters/agent/user_access.md) keyword.
- Self-managed only. KAS is running on the GitLab subdomain. For example, `kas.example.com` and `example.com`.
### The environment already exists

View File

@ -106,7 +106,7 @@ Reviewer roulette is an internal tool for use on GitLab.com, and not available f
The [Danger bot](dangerbot.md) randomly picks a reviewer and a maintainer for
each area of the codebase that your merge request seems to touch. It makes
**recommendations** for developer reviewers and you should override it if you think someone else is a better
fit.
fit.
We only do UX reviews for MRs from teams that include a Product Designer. User-facing changes from these teams are required to have a UX review, even if it's behind a feature flag. Default to the recommended UX reviewer suggested.

View File

@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Database Reviews
- During the design phase of the feature you're working on, be mindful if you are adding any database-related changes. If you're adding or modifying a query, start looking at the `explain` plan early to avoid surprises late in the review phase.
- During the design phase of the feature you're working on, be mindful if you are adding any database-related changes. If you're adding or modifying a query, start looking at the `explain` plan early to avoid surprises late in the review phase.
- If, at any time, you need help optimizing a query or understanding an `explain` plan, ask for assistance in `#database`.
- If you're creating a database MR for review, check out our [Database review guidelines](../database_review.md).

View File

@ -21,7 +21,7 @@ is implemented in GitLab Rails and Sidekiq.
## Components
F few Ruby classes are involved in the load balancing process. All of them are
A few Ruby classes are involved in the load balancing process. All of them are
in the namespace `Gitlab::Database::LoadBalancing`:
1. `Host`

View File

@ -45,6 +45,20 @@ Using Gems can provide several benefits for code maintenance:
Since the gem is packaged, not changed too often, it also allows us to run those tests less frequently improving
CI testing time.
## Gem naming
Gems can fall under three different case:
- `unique_gem`: Don't include `gitlab` in the gem name if the gem doesn't include anything specific to GitLab
- `existing_gem-gitlab`: When you fork and modify/extend a publicly available gem, add the `-gitlab` suffix, according to [Rubygems' convention](https://guides.rubygems.org/name-your-gem/)
- `gitlab-unique_gem`: Include a `gitlab-` prefix to gems that are only useful in the context of GitLab projects.
Examples of existing gems:
- `y-rb`: Ruby bindings for yrs. Yrs "wires" is a Rust port of the Yjs framework.
- `activerecord-gitlab`: Adds GitLab-specific patches to the `activerecord` public gem.
- `gitlab-rspec` and `gitlab-utils`: GitLab-specific set of classes to help in a particular context, or re-use code.
## In the same repo
**Our GitLab Gems should be always put in `gems/` of GitLab monorepo.**
@ -57,23 +71,24 @@ They should not be published to RubyGems.
### Create and use a new Gem
You can see example adding new Gem: [!121676](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121676).
You can see example adding a new gem: [!121676](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121676).
1. Create a new Ruby Gem in `gems/gitlab-<name-of-gem>` with `bundle gem gems/gitlab-<name-of-gem> --no-exe --no-coc --no-ext --no-mit`.
1. Remove the `.git` folder in `gems/gitlab-<name-of-gem>` with `rm -rf gems/gitlab-<name-of-gem>/.git`.
1. Edit `gitlab-<name-of-gem>/README.md` to provide a simple description of the Gem.
1. Edit `gitlab-<name-of-gem>/gitlab-<name-of-gem>.gemspec` and fill the details about the Gem as in the following example:
1. Pick a good name for the gem, by following the [Gem naming](#gem-naming) convention.
1. Create the new gem in `gems/<name-of-gem>` with `bundle gem gems/<name-of-gem> --no-exe --no-coc --no-ext --no-mit`.
1. Remove the `.git` folder in `gems/<name-of-gem>` with `rm -rf gems/<name-of-gem>/.git`.
1. Edit `gems/<name-of-gem>/README.md` to provide a simple description of the Gem.
1. Edit `gems/<name-of-gem>/<name-of-gem>.gemspec` and fill the details about the Gem as in the following example:
```ruby
Gem::Specification.new do |spec|
spec.name = "gitlab-<name-of-gem>"
spec.name = "<name-of-gem>"
spec.version = Gitlab::NameOfGem::VERSION
spec.authors = ["group::tenant-scale"]
spec.email = ["engineering@gitlab.com"]
spec.summary = "GitLab's RSpec extensions"
spec.description = "A set of useful helpers to configure RSpec with various stubs and CI configs."
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-<name-of-gem>"
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/<name-of-gem>"
spec.required_ruby_version = ">= 2.7"
end
```
@ -174,7 +189,7 @@ usage, adding consistency checks and various helpers to track owners of feature
not really part of GitLab business logic and could be used to better track our implementation
of Flipper and possibly much easier change it to dogfood [GitLab Feature Flags](../operations/feature_flags.md).
The `gitlab-active_record` is a gem adding GitLab specific Active Record patches.
The `activerecord-gitlab` is a gem adding GitLab specific Active Record patches.
It is very well desired for such to be managed separately to isolate complexity.
### Other potential use cases

View File

@ -14,7 +14,7 @@ Feature flags help reduce risk, allowing you to do controlled testing, and separ
delivery from customer launch.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an example of feature flags in action, see [GitLab for deploys, feature flags, and error tracking](https://www.youtube.com/watch?v=5tw2p6lwXxo).
For an example of feature flags in action, see [Feature Flags configuration, instrumentation and use](https://www.youtube.com/watch?v=ViA6suScxkE).
NOTE:
To contribute to the development of the GitLab product, view

View File

@ -185,7 +185,7 @@ This table shows available scopes per token. Scopes can be limited further on to
1. You can also store token using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
1. Do not:
- Store tokens in plain text in your projects.
- Include tokens when pasting code, console commands, or log outputs into an issue, MR description, or comment.
- Include tokens when pasting code, console commands, or log outputs into an issue, MR description, or comment.
Consider an approach such as [using external secrets in CI](../ci/secrets/index.md).
1. Do not log credentials in the console logs or artifacts. Consider [protecting](../ci/variables/index.md#protect-a-cicd-variable) and
[masking](../ci/variables/index.md#mask-a-cicd-variable) your credentials.
@ -194,5 +194,5 @@ This table shows available scopes per token. Scopes can be limited further on to
- Personal, project, and group access tokens.
- Feed tokens.
- Trigger tokens.
- Runner registration tokens.
- Runner registration tokens.
- Any other sensitive secrets etc.

View File

@ -28,7 +28,7 @@ Write code more efficiently by using generative AI to suggest code while you're
Code Suggestions are available:
- To users of GitLab SaaS (by default) and self-managed GitLab (when requested).
- In VS Code and Microsoft Visual Studio when you have the corresponding GitLab extension installed.
- In VS Code and Microsoft Visual Studio when you have the corresponding GitLab extension installed.
- In the GitLab WebIDE
<div class="video-fallback">
@ -54,7 +54,7 @@ The best results from Code Suggestions are expected [for languages the Google Ve
- Python
- TypeScript
Supported [code infrastructure interfaces](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_code_infrastructure_interfaces) include:
Supported [code infrastructure interfaces](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_code_infrastructure_interfaces) include:
- Google Cloud CLI
- Kubernetes Resource Model (KRM)
@ -175,7 +175,7 @@ Suggestions are best when writing new code. Editing existing functions or 'fill
GitLab is making improvements to the Code Suggestions to improve the quality. AI is non-deterministic, so you may not get the same suggestion every time with the same input.
This feature is currently in [Beta](../../../policy/experiment-beta-support.md#beta).
Code Suggestions depends on both Google Vertex AI Codey APIs and the GitLab Code Suggestions service. We expect a
Code Suggestions depends on both Google Vertex AI Codey APIs and the GitLab Code Suggestions service. We expect a
high demand for this Beta feature, which may cause degraded performance or unexpected downtime
of the feature. We have built this feature to gracefully degrade and have controls in place to allow us to
mitigate abuse or misuse. GitLab may disable this feature for any or all customers at any time at our discretion.
@ -244,9 +244,9 @@ We strongly encourage all beta users to leverage GitLab native
[Code Quality Scanning](../../../ci/testing/code_quality.md) and
[Security Scanning](../../application_security/index.md) capabilities.
GitLab currently does not retrain Google Vertex AI Codey APIs. GitLab makes no claims
to the accuracy or quality of code suggestions generated by Google Vertex AI Codey API.
Read more about [Google Vertex AI foundation model capabilities](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models).
GitLab currently does not retrain Google Vertex AI Codey APIs. GitLab makes no claims
to the accuracy or quality of code suggestions generated by Google Vertex AI Codey API.
Read more about [Google Vertex AI foundation model capabilities](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models).
## Known limitations

View File

@ -21,6 +21,7 @@ module API
expose :full_name, :full_path
expose :created_at
expose :parent_id
expose :shared_runners_setting
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes

View File

@ -55,7 +55,14 @@ module API
end
expose :moved_to_id
expose :service_desk_reply_to
expose :service_desk_reply_to do |issue|
issue.present(
current_user: options[:current_user],
# We need to pass it explicitly to account for the case where `issue`
# is a `WorkItem` which doesn't have a presenter yet.
presenter_class: IssuePresenter
).service_desk_reply_to
end
end
end
end

View File

@ -251,6 +251,28 @@ module API
present_projects load_projects
end
desc 'Get projects that a user has contributed to' do
success code: 200, model: Entities::BasicProjectDetails
failure [{ code: 404, message: '404 User Not Found' }]
tags %w[projects]
is_array true
end
params do
requires :user_id, type: String, desc: 'The ID or username of the user'
use :sort_params
use :pagination
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
end
get ":user_id/contributed_projects", feature_category: :groups_and_projects, urgency: :low do
user = find_user(params[:user_id])
not_found!('User') unless user
contributed_projects = ContributedProjectsFinder.new(user).execute(current_user).joined(user)
present_projects contributed_projects
end
desc 'Get projects starred by a user' do
success code: 200, model: Entities::BasicProjectDetails
failure [{ code: 404, message: '404 User Not Found' }]

View File

@ -29,15 +29,19 @@ module Banzai
@references_per_parent[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
prepare_doc_for_scan.to_enum(:scan, regex).each do
parent_path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
[filter.object_class.link_reference_pattern, filter.object_class.reference_pattern].each do |pattern|
next unless pattern
ident = filter.identifier($~)
refs[parent_path] << ident if ident
prepare_doc_for_scan.to_enum(:scan, pattern).each do
parent_path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
ident = filter.identifier($~)
refs[parent_path] << ident if ident
end
end
refs
@ -171,15 +175,6 @@ module Banzai
delegate :project, :group, :parent, :parent_type, to: :filter
def regex
strong_memoize(:regex) do
[
filter.object_class.link_reference_pattern,
filter.object_class.reference_pattern
].compact.reduce { |a, b| Regexp.union(a, b) }
end
end
def refs_cache
Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
end

View File

@ -6,7 +6,8 @@ module Gitlab
include Sidekiq::ServerMiddleware
def call(worker, job, queue)
logger.info "arguments: #{Gitlab::Json.dump(job['args'])}"
loggable_args = Gitlab::ErrorTracking::Processor::SidekiqProcessor.loggable_arguments(job['args'], job['class'])
logger.info "arguments: #{Gitlab::Json.dump(loggable_args)}"
yield
end
end

View File

@ -1962,6 +1962,9 @@ msgstr ""
msgid "AI|May provide inappropriate responses not representative of GitLab's views. Do not input personal data."
msgstr ""
msgid "AI|New chat"
msgstr ""
msgid "AI|Populate issue description"
msgstr ""
@ -2442,9 +2445,6 @@ msgstr ""
msgid "AccessTokens|Created"
msgstr ""
msgid "AccessTokens|Enter the name of your application, and we'll return a unique %{type}."
msgstr ""
msgid "AccessTokens|Feed token"
msgstr ""
@ -31273,6 +31273,9 @@ msgstr ""
msgid "Notify|Committed by"
msgstr ""
msgid "Notify|Could not find the following %{column} values in %{project}%{parent_groups_clause}: %{error_lines}"
msgstr ""
msgid "Notify|Don't want to receive updates from GitLab administrators?"
msgstr ""
@ -31390,6 +31393,9 @@ msgstr ""
msgid "Notify|Please check that your service provider supports email subaddressing and that you have set up email forwarding correctly."
msgstr ""
msgid "Notify|Please fix the errors above and try the CSV import again."
msgstr ""
msgid "Notify|Please fix the lines with errors and try the CSV import again."
msgstr ""

View File

@ -0,0 +1,16 @@
diff --git a/node_modules/@rails/ujs/lib/assets/compiled/rails-ujs.js b/node_modules/@rails/ujs/lib/assets/compiled/rails-ujs.js
index d428163..010eaa5 100644
--- a/node_modules/@rails/ujs/lib/assets/compiled/rails-ujs.js
+++ b/node_modules/@rails/ujs/lib/assets/compiled/rails-ujs.js
@@ -281,11 +281,6 @@ Released under the MIT license
try {
response = JSON.parse(response);
} catch (error) {}
- } else if (type.match(/\b(?:java|ecma)script\b/)) {
- script = document.createElement('script');
- script.setAttribute('nonce', cspNonce());
- script.text = response;
- document.head.appendChild(script).parentNode.removeChild(script);
} else if (type.match(/\b(xml|html|svg)\b/)) {
parser = new DOMParser();
type = type.replace(/;.+/, '');

View File

@ -55,12 +55,13 @@ RSpec.describe Admin::HooksController do
hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
hook_params = {
url: 'http://example.com/{baz}?token={token}',
url: 'http://example.com/{bar}?token={token}',
enable_ssl_verification: false,
url_variables: [
{ key: 'token', value: 'some secret value' },
{ key: 'baz', value: 'qux' },
{ key: 'foo', value: nil }
{ key: 'baz', value: nil },
{ key: 'foo', value: nil },
{ key: 'bar', value: 'qux' }
]
}
@ -72,7 +73,7 @@ RSpec.describe Admin::HooksController do
expect(flash[:notice]).to include('was updated')
expect(hook).to have_attributes(hook_params.except(:url_variables))
expect(hook).to have_attributes(
url_variables: { 'token' => 'some secret value', 'baz' => 'qux' }
url_variables: { 'token' => 'some secret value', 'bar' => 'qux' }
)
end
end

View File

@ -395,6 +395,12 @@ RSpec.describe Import::GithubController, feature_category: :importers do
)
end
let(:user) { project.owner }
before do
sign_in(user)
end
context 'when import is not finished' do
it 'return bad_request' do
get :failures, params: { project_id: project.id }
@ -434,6 +440,16 @@ RSpec.describe Import::GithubController, feature_category: :importers do
expect(json_response.first['title']).to eq(issue_title)
end
end
context 'when signed user is not the owner' do
let(:user) { create(:user) }
it 'renders 404' do
get :failures, params: { project_id: project.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe "POST cancel" do
@ -444,6 +460,12 @@ RSpec.describe Import::GithubController, feature_category: :importers do
)
end
let(:user) { project.owner }
before do
sign_in(user)
end
context 'when project import was canceled' do
before do
allow(Import::Github::CancelProjectImportService)
@ -476,6 +498,16 @@ RSpec.describe Import::GithubController, feature_category: :importers do
expect(json_response['errors']).to eq('The import cannot be canceled because it is finished')
end
end
context 'when signed user is not the owner' do
let(:user) { create(:user) }
it 'renders 404' do
post :cancel, params: { project_id: project.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'POST cancel_all' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::CompareController do
RSpec.describe Projects::CompareController, feature_category: :source_code_management do
include ProjectForksHelper
using RSpec::Parameterized::TableSyntax
@ -211,6 +211,36 @@ RSpec.describe Projects::CompareController do
end
end
context 'when the target project is the default source but hidden to the user' do
let(:project) { create(:project, :repository, :private) }
let(:from_ref) { 'improve%2Fmore-awesome' }
let(:to_ref) { 'feature' }
let(:whitespace) { nil }
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
from: from_ref,
to: to_ref,
w: whitespace,
page: page,
straight: straight
}
end
it 'does not show the diff' do
allow(controller).to receive(:source_project).and_return(project)
expect(project).to receive(:default_merge_request_target).and_return(private_fork)
show_request
expect(response).to be_successful
expect(assigns(:diffs)).to be_empty
expect(assigns(:commits)).to be_empty
end
end
context 'when the source ref does not exist' do
let(:from_project_id) { nil }
let(:from_ref) { 'non-existent-source-ref' }

View File

@ -504,7 +504,9 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
end
context 'when user has an unconfirmed email', :js do
let(:unconfirmed_user) { create(:user, :unconfirmed) }
# Email address contains HTML to ensure email address is displayed in an HTML safe way.
let_it_be(:unconfirmed_email) { "#{generate(:email)}<h2>testing<img/src=http://localhost:8000/test.png>" }
let_it_be(:unconfirmed_user) { create(:user, :unconfirmed, unconfirmed_email: unconfirmed_email) }
where(:path_helper) do
[
@ -524,7 +526,9 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
within_modal do
expect(page).to have_content("Confirm user #{unconfirmed_user.name}?")
expect(page).to have_content('This user has an unconfirmed email address. You may force a confirmation.')
expect(page).to have_content(
"This user has an unconfirmed email address (#{unconfirmed_email}). You may force a confirmation."
)
click_button 'Confirm user'
end

View File

@ -0,0 +1,5 @@
title,description,milestone
"Issue with missing milestone","",15.10,
"Issue without milestone","",,
"Issue with milestone","",10.1,
"Issue with duplicate milestone","",15.10,
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -44,7 +44,7 @@ describe('TokensApp', () => {
const container = extendedWrapper(wrapper.findByTestId(testId));
expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true);
expect(container.findByText(expectedDescription).exists()).toBe(true);
expect(container.findByText(expectedDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText('reset this token').attributes()).toMatchObject({
'data-confirm': expectedResetConfirmMessage,

View File

@ -73,7 +73,7 @@ describe('RemoveAvatar', () => {
let formSubmitSpy;
beforeEach(() => {
formSubmitSpy = jest.spyOn(wrapper.vm.$refs.deleteForm, 'submit');
formSubmitSpy = jest.spyOn(findForm().element, 'submit');
findModal().vm.$emit('primary');
});

View File

@ -162,14 +162,14 @@ describe('gl_emoji', () => {
]);
window.gon = { features: { customEmoji: true } };
document.body.dataset.group = 'test-group';
document.body.dataset.groupFullPath = 'test-group';
await initEmojiMock(emojiData);
});
afterEach(() => {
window.gon = {};
delete document.body.dataset.group;
delete document.body.dataset.groupFullPath;
});
it('renders custom emoji', async () => {

View File

@ -72,7 +72,7 @@ function createMockEmojiClient() {
]);
window.gon = { features: { customEmoji: true } };
document.body.dataset.group = 'test-group';
document.body.dataset.groupFullPath = 'test-group';
}
describe('emoji', () => {
@ -82,7 +82,7 @@ describe('emoji', () => {
afterEach(() => {
window.gon = {};
delete document.body.dataset.group;
delete document.body.dataset.groupFullPath;
clearEmojiMock();
});
@ -758,7 +758,7 @@ describe('emoji', () => {
describe('when not in a group', () => {
beforeEach(() => {
delete document.body.dataset.group;
delete document.body.dataset.groupFullPath;
});
it('returns empty object', async () => {

View File

@ -91,7 +91,7 @@ describe('UserNameGroup component', () => {
});
it('should render status message', () => {
expect(findUserStatus().text()).toContain(userMenuMockData.status.message);
expect(findUserStatus().html()).toContain(userMenuMockData.status.message_html);
});
it("sets the tooltip's target to the status container", () => {

View File

@ -126,6 +126,7 @@ export const userMenuMockStatus = {
customized: false,
emoji: 'art',
message: 'Working on user menu in super sidebar',
message_html: '<gl-emoji></gl-emoji> Working on user menu in super sidebar',
availability: 'busy',
clear_after: '2023-02-09 20:06:35 UTC',
};

View File

@ -8,6 +8,7 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
jest.mock('fuzzaldrin-plus', () => ({
@ -38,6 +39,7 @@ const mockFiles = [
describe('Diff Stats Dropdown', () => {
let wrapper;
const focusInputMock = jest.fn();
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
wrapper = shallowMountExtended(DiffStatsDropdown, {
@ -50,6 +52,9 @@ describe('Diff Stats Dropdown', () => {
stubs: {
GlSprintf,
GlDropdown,
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
methods: { focusInput: focusInputMock },
}),
},
});
};
@ -151,10 +156,8 @@ describe('Diff Stats Dropdown', () => {
});
it('should set the search input focus', () => {
wrapper.vm.$refs.search.focusInput = jest.fn();
findChanged().vm.$emit('shown');
expect(wrapper.vm.$refs.search.focusInput).toHaveBeenCalled();
expect(focusInputMock).toHaveBeenCalled();
});
});
});

View File

@ -909,6 +909,14 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(forks).to contain_exactly(a_hash_including('fullPath' => fork_developer.full_path),
a_hash_including('fullPath' => fork_group_developer.full_path))
end
context 'when current user is not set' do
let(:user) { nil }
it 'does not return any forks' do
expect(forks.count).to eq(0)
end
end
end
end
end

View File

@ -433,7 +433,8 @@ RSpec.describe ApplicationHelper do
page: 'application',
page_type_id: nil,
find_file: nil,
group: nil
group: nil,
group_full_path: nil
}
)
end
@ -449,7 +450,8 @@ RSpec.describe ApplicationHelper do
page: 'application',
page_type_id: nil,
find_file: nil,
group: group.path
group: group.path,
group_full_path: group.full_path
}
)
end
@ -473,6 +475,7 @@ RSpec.describe ApplicationHelper do
page_type_id: nil,
find_file: nil,
group: nil,
group_full_path: nil,
project_id: project.id,
project: project.path,
namespace_id: project.namespace.id
@ -491,6 +494,7 @@ RSpec.describe ApplicationHelper do
page_type_id: nil,
find_file: nil,
group: project.group.name,
group_full_path: project.group.full_path,
project_id: project.id,
project: project.path,
namespace_id: project.namespace.id
@ -517,6 +521,7 @@ RSpec.describe ApplicationHelper do
page_type_id: issue.id,
find_file: nil,
group: nil,
group_full_path: nil,
project_id: issue.project.id,
project: issue.project.path,
namespace_id: issue.project.namespace.id

View File

@ -106,7 +106,8 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
customized: user.status&.customized?,
availability: user.status&.availability.to_s,
emoji: user.status&.emoji,
message: user.status&.message_html&.html_safe,
message_html: user.status&.message_html&.html_safe,
message: user.status&.message&.html_safe,
clear_after: nil
},
settings: {

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::API::Entities::Issue, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let(:issue) { build_stubbed(:issue, project: project) }
let(:current_user) { build_stubbed(:user) }
let(:options) { { current_user: current_user }.merge(option_addons) }
let(:option_addons) { {} }
let(:entity) { described_class.new(issue, options) }
subject(:json) { entity.as_json }
describe '#service_desk_reply_to', feature_category: :service_desk do
# Setting to true (default) doesn't play nice with stubs
let(:option_addons) { { include_subscribed: false } }
let(:issue) { build_stubbed(:issue, project: project, service_desk_reply_to: email) }
let(:email) { 'creator@example.com' }
let(:role) { :developer }
subject { json[:service_desk_reply_to] }
context 'as developer' do
before do
stub_member_access_level(issue.project, developer: current_user)
end
it { is_expected.to eq(email) }
end
context 'as guest' do
before do
stub_member_access_level(issue.project, guest: current_user)
end
it { is_expected.to eq('cr*****@e*****.c**') }
end
context 'without email' do
let(:email) { nil }
specify { expect(json).to have_key(:service_desk_reply_to) }
it { is_expected.to eq(nil) }
end
end
end

View File

@ -36,6 +36,15 @@ RSpec.describe Emails::Issues, feature_category: :team_planning do
expect(subject).to have_body_text "23, 34, 58"
end
it "shows issuable errors with column" do
@results = { success: 0, error_lines: [], parse_error: false,
preprocess_errors:
{ milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 15.11] } } } }
expect(subject).to have_body_text "Could not find the following milestone values in \
#{project.full_name}: 15.10, 15.11"
end
context 'with header and footer' do
let(:results) { { success: 165, error_lines: [], parse_error: false } }

View File

@ -258,6 +258,13 @@ RSpec.describe WebHook, feature_category: :webhooks do
expect(hook.url_variables).to eq({})
end
it 'resets url variables if url variables are overwritten' do
hook.url_variables = hook.url_variables.merge('abc' => 'baz')
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
it 'does not reset url variables if both url and url variables are changed' do
hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }

View File

@ -3,10 +3,20 @@
require 'spec_helper'
RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tracking do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:setting) { create(:project_error_tracking_setting, project: project) }
let_it_be(:project_without_setting) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:non_member) { create(:user) }
let(:user) { maintainer }
before_all do
project.add_developer(developer)
project.add_maintainer(maintainer)
project_without_setting.add_developer(developer)
project_without_setting.add_maintainer(maintainer)
end
shared_examples 'returns project settings' do
it 'returns correct project settings' do
@ -108,10 +118,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'when authenticated as maintainer' do
before do
project.add_maintainer(user)
end
context 'with integrated_error_tracking feature enabled' do
it_behaves_like 'returns project settings'
end
@ -179,10 +185,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
context 'without a project setting' do
let(:project) { project_without_setting }
before do
project.add_maintainer(user)
end
it_behaves_like 'returns no project settings'
end
@ -208,14 +210,14 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'when authenticated as developer' do
before do
project.add_developer(user)
end
let(:user) { developer }
it_behaves_like 'returns 403'
end
context 'when authenticated as non-member' do
let(:user) { non_member }
it_behaves_like 'returns 404'
end
@ -232,10 +234,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'when authenticated as maintainer' do
before do
project.add_maintainer(user)
end
it_behaves_like 'returns project settings'
context 'when integrated_error_tracking feature disabled' do
@ -250,22 +248,18 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
context 'without a project setting' do
let(:project) { project_without_setting }
before do
project.add_maintainer(user)
end
it_behaves_like 'returns no project settings'
end
context 'when authenticated as developer' do
before do
project.add_developer(user)
end
let(:user) { developer }
it_behaves_like 'returns 403'
end
context 'when authenticated as non-member' do
let(:user) { non_member }
it_behaves_like 'returns 404'
end
@ -287,14 +281,8 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
context 'when authenticated' do
context 'as maintainer' do
before do
project.add_maintainer(user)
end
context "when integrated" do
context "with existing setting" do
let(:project) { setting.project }
let(:setting) { create(:project_error_tracking_setting, :integrated) }
let(:active) { false }
it "updates a setting" do
@ -302,13 +290,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(
"active" => false,
"api_url" => nil,
"integrated" => integrated,
"project_name" => nil,
"sentry_external_url" => nil
)
expect(json_response).to include("integrated" => true)
end
end
@ -366,14 +348,14 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context "as developer" do
before do
project.add_developer(user)
end
let(:user) { developer }
it_behaves_like 'returns 403'
end
context 'as non-member' do
let(:user) { non_member }
it_behaves_like 'returns 404'
end
end

View File

@ -844,6 +844,39 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
end
end
context "expose shared_runners_setting attribute" do
let(:group) { create(:group, shared_runners_enabled: true) }
before do
group.add_owner(user1)
end
it "returns the group with shared_runners_setting as 'enabled'", :aggregate_failures do
get api("/groups/#{group.id}", user1)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['shared_runners_setting']).to eq("enabled")
end
it "returns the group with shared_runners_setting as 'disabled_and_unoverridable'", :aggregate_failures do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false)
get api("/groups/#{group.id}", user1)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['shared_runners_setting']).to eq("disabled_and_unoverridable")
end
it "returns the group with shared_runners_setting as 'disabled_and_overridable'", :aggregate_failures do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true)
get api("/groups/#{group.id}", user1)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['shared_runners_setting']).to eq("disabled_and_overridable")
end
end
end
end

View File

@ -1835,6 +1835,72 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
end
end
describe 'GET /users/:user_id/contributed_projects/' do
let(:path) { "/users/#{user3.id}/contributed_projects/" }
let_it_be(:project1) { create(:project, :public, path: 'my-project') }
let_it_be(:project2) { create(:project, :public) }
let_it_be(:project3) { create(:project, :public) }
let_it_be(:private_project) { create(:project, :private) }
before do
private_project.add_maintainer(user3)
create(:push_event, project: project1, author: user3)
create(:push_event, project: project2, author: user3)
create(:push_event, project: private_project, author: user3)
end
it 'returns error when user not found' do
get api("/users/#{non_existing_record_id}/contributed_projects/", user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
context 'with a public profile' do
it 'returns projects filtered by user' do
get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] })
.to contain_exactly(project1.id, project2.id)
end
end
context 'with a private profile' do
before do
user3.update!(private_profile: true)
user3.reload
end
context 'user does not have access to view the private profile' do
it 'returns no projects' do
get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to be_empty
end
end
context 'user has access to view the private profile as an admin' do
it 'returns projects filtered by user' do
get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] })
.to contain_exactly(project1.id, project2.id, private_project.id)
end
end
end
end
describe 'POST /projects/user/:id' do
let(:path) { "/projects/user/#{user.id}" }

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ImportCsv::PreprocessMilestonesService, feature_category: :importers do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:provided_titles) { %w[15.10 10.1] }
let(:service) { described_class.new(user, project, provided_titles) }
subject { service.execute }
describe '#execute' do
let(:project_milestones) { ::MilestonesFinder.new({ project_ids: [project.id] }).execute }
shared_examples 'csv import' do |is_success:, milestone_errors:|
it 'does not create milestones' do
expect { subject }.not_to change { project_milestones.count }
end
it 'reports any missing milestones' do
result = subject
if is_success
expect(result).to be_success
else
expect(result[:status]).to eq(:error)
expect(result.payload).to match(milestone_errors)
end
end
end
context 'with csv that has missing or unavailable milestones' do
it_behaves_like 'csv import',
{ is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 10.1] } } }
end
context 'with csv that includes project milestones' do
let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }
it_behaves_like 'csv import',
{ is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
end
context 'with csv that includes milestones column' do
let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }
context 'when milestones exist in the importing projects group' do
let!(:group_milestone) { create(:milestone, group: group, title: '10.1') }
it_behaves_like 'csv import', { is_success: true, milestone_errors: nil }
end
context 'when milestones exist in a subgroup of the importing projects group' do
let_it_be(:subgroup) { create(:group, parent: group) }
let!(:group_milestone) { create(:milestone, group: subgroup, title: '10.1') }
it_behaves_like 'csv import',
{ is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
end
context 'when milestones exist in a different project from the importing project' do
let_it_be(:second_project) { create(:project, group: group) }
let!(:second_project_milestone) { create(:milestone, project: second_project, title: '10.1') }
it_behaves_like 'csv import',
{ is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
end
context 'when duplicate milestones exist in the projects group and parent group' do
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:project) { create(:project, group: sub_group) }
let!(:ancestor_group_milestone) { create(:milestone, group: group, title: '15.10') }
let!(:ancestor_group_milestone_two) { create(:milestone, group: group, title: '10.1') }
let!(:group_milestone) { create(:milestone, group: sub_group, title: '10.1') }
it_behaves_like 'csv import', { is_success: true, milestone_errors: nil }
end
end
end
end

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Issuable::ImportCsv::BaseService, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:csv_io) { double }
let(:importer_klass) do
Class.new(described_class) do
def email_results_to_user
# no-op
end
end
end
let(:service) do
uploader = FileUploader.new(project)
uploader.store!(file)
importer_klass.new(user, project, uploader)
end
subject { service.execute }
describe '#preprocess_milestones' do
let(:utility_class) { ImportCsv::PreprocessMilestonesService }
let(:file) { fixture_file_upload('spec/fixtures/csv_missing_milestones.csv') }
let(:mocked_object) { double }
before do
allow(service).to receive(:create_object).and_return(mocked_object)
allow(mocked_object).to receive(:persisted?).and_return(true)
end
context 'with csv that has milestone heading' do
before do
allow(utility_class).to receive(:new).and_return(utility_class)
allow(utility_class).to receive(:execute).and_return(ServiceResponse.success)
end
it 'calls PreprocessMilestonesService' do
subject
expect(utility_class).to have_received(:new)
end
it 'calls PreprocessMilestonesService with unique milestone titles' do
subject
expect(utility_class).to have_received(:new).with(user, project, %w[15.10 10.1])
expect(utility_class).to have_received(:execute)
end
end
context 'with csv that does not have milestone heading' do
let(:file) { fixture_file_upload('spec/fixtures/work_items_valid_types.csv') }
before do
allow(utility_class).to receive(:new).and_return(utility_class)
end
it 'does not call PreprocessMilestonesService' do
subject
expect(utility_class).not_to have_received(:new)
end
end
context 'when one or more milestones do not exist' do
it 'returns the expected error in results payload' do
results = subject
expect(results[:success]).to eq(0)
expect(results[:preprocess_errors]).to match({
milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 10.1] } }
})
end
end
context 'when all milestones exist' do
let!(:group_milestone) { create(:milestone, group: group, title: '10.1') }
let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }
it 'returns a successful response' do
results = subject
expect(results[:preprocess_errors]).to be_empty
expect(results[:success]).to eq(4)
end
end
end
end

View File

@ -30,6 +30,7 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type|
context 'with a file generated by Gitlab CSV export' do
let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
let!(:test_milestone) { create(:milestone, project: project, title: 'v1.0') }
it 'imports the CSV without errors' do
expect(subject[:success]).to eq(4)

View File

@ -8,6 +8,10 @@ RSpec.describe 'notify/import_issues_csv_email.html.haml' do
let(:correct_results) { { success: 3, parse_error: false } }
let(:errored_results) { { success: 3, error_lines: [5, 6, 7], parse_error: false } }
let(:parse_error_results) { { success: 0, parse_error: true } }
let(:milestone_error_results) do
{ success: 0,
preprocess_errors: { milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 15.11] } } } }
end
before do
assign(:user, user)
@ -58,4 +62,29 @@ a delimited text file that uses a comma to separate values.")
Please make sure it has the correct format: a delimited text file that uses a comma to separate values.")
end
end
context 'when preprocess errors reported while importing' do
before do
assign(:results, milestone_error_results)
end
it 'renders with project name error' do
render
expect(rendered).to have_content("Could not find the following milestone values in \
#{project.full_name}: 15.10, 15.11")
end
context 'with a project in a group' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
it 'renders with group clause error' do
render
expect(rendered).to have_content("Could not find the following milestone values in #{project.full_name} \
or its parent groups: 15.10, 15.11")
end
end
end
end