Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-16 12:07:25 +00:00
parent 5b9147d835
commit f962f7d316
92 changed files with 1582 additions and 271 deletions

View File

@ -83,7 +83,6 @@ Gitlab/AvoidGitlabInstanceChecks:
- 'ee/app/workers/gitlab_subscriptions/schedule_refresh_seats_worker.rb'
- 'ee/app/workers/update_all_mirrors_worker.rb'
- 'ee/lib/api/code_suggestions.rb'
- 'ee/lib/api/internal/upcoming_reconciliations.rb'
- 'ee/lib/api/scim/instance_scim.rb'
- 'ee/lib/ee/api/namespaces.rb'
- 'ee/lib/ee/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb'

View File

@ -3500,7 +3500,6 @@ Gitlab/BoundedContexts:
- 'ee/app/services/timebox/report_service.rb'
- 'ee/app/services/timebox_report_service.rb'
- 'ee/app/services/todos/destroy/confidential_epic_service.rb'
- 'ee/app/services/upcoming_reconciliations/update_service.rb'
- 'ee/app/services/user_permissions/export_service.rb'
- 'ee/app/services/users_ops_dashboard_projects/base_service.rb'
- 'ee/app/services/users_ops_dashboard_projects/destroy_service.rb'

View File

@ -10,7 +10,6 @@ Layout/FirstHashElementIndentation:
- 'ee/spec/models/concerns/elastic/note_spec.rb'
- 'ee/spec/requests/api/analytics/project_deployment_frequency_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/iterations/create_spec.rb'
- 'ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb'
- 'ee/spec/requests/api/merge_requests_spec.rb'
- 'ee/spec/requests/ee/projects/deploy_tokens_controller_spec.rb'
- 'ee/spec/requests/groups/group_members_controller_spec.rb'

View File

@ -1812,7 +1812,6 @@ Layout/LineLength:
- 'ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb'
- 'ee/spec/requests/api/internal/base_spec.rb'
- 'ee/spec/requests/api/internal/kubernetes_spec.rb'
- 'ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb'
- 'ee/spec/requests/api/invitations_spec.rb'
- 'ee/spec/requests/api/issues_spec.rb'
- 'ee/spec/requests/api/iterations_spec.rb'

View File

@ -56,7 +56,6 @@ Performance/MapCompact:
- 'ee/app/services/members/activate_service.rb'
- 'ee/app/services/security/override_uuids_service.rb'
- 'ee/app/services/security/store_scan_service.rb'
- 'ee/app/services/upcoming_reconciliations/update_service.rb'
- 'ee/app/services/vulnerabilities/findings/find_or_create_from_security_finding_service.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/lib/banzai/filter/references/iteration_reference_filter.rb'

View File

@ -758,7 +758,6 @@ RSpec/ContextWording:
- 'ee/spec/services/system_notes/vulnerabilities_service_spec.rb'
- 'ee/spec/services/timebox_report_service_spec.rb'
- 'ee/spec/services/todo_service_spec.rb'
- 'ee/spec/services/upcoming_reconciliations/update_service_spec.rb'
- 'ee/spec/services/user_permissions/export_service_spec.rb'
- 'ee/spec/services/vulnerability_exports/export_service_spec.rb'
- 'ee/spec/support/features/manual_quarterly_co_term_banner_examples.rb'

View File

@ -58,7 +58,6 @@ RSpec/ExpectChange:
- 'ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
- 'ee/spec/requests/api/groups_spec.rb'
- 'ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb'
- 'ee/spec/requests/api/issues_spec.rb'
- 'ee/spec/requests/api/provider_identity_spec.rb'
- 'ee/spec/requests/groups/epics/related_epic_links_controller_spec.rb'

View File

@ -64,7 +64,6 @@ RSpec/ExpectInHook:
- 'ee/spec/services/projects/create_from_template_service_spec.rb'
- 'ee/spec/services/projects/mark_for_deletion_service_spec.rb'
- 'ee/spec/services/projects/update_mirror_service_spec.rb'
- 'ee/spec/services/upcoming_reconciliations/update_service_spec.rb'
- 'ee/spec/support/shared_examples/lib/gitlab/graphql/issuables_lazy_links_aggregate_shared_examples.rb'
- 'ee/spec/support/shared_examples/models/concerns/elastic/cannot_read_cross_project_shared_examples.rb'
- 'ee/spec/support/shared_examples/models/concerns/verifiable_replicator_shared_examples.rb'

View File

@ -25,7 +25,6 @@ Style/SingleArgumentDig:
- 'ee/spec/requests/api/graphql/project/dast_site_profile_spec.rb'
- 'ee/spec/requests/api/graphql/project/dast_site_profiles_spec.rb'
- 'ee/spec/requests/api/graphql/project/requirements_management/requirements_spec.rb'
- 'ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb'
- 'ee/spec/services/vulnerabilities/findings/find_or_create_from_security_finding_service_spec.rb'
- 'ee/spec/services/vulnerabilities/manually_create_service_spec.rb'
- 'lib/gitlab/auth/o_auth/auth_hash.rb'

View File

@ -1,4 +1,5 @@
import { Mark } from '@tiptap/core';
import { Fragment } from '@tiptap/pm/model';
import Code from '@tiptap/extension-code';
import { EXTENSION_PRIORITY_LOWER } from '../constants';
@ -12,6 +13,18 @@ export default Code.extend({
*/
priority: EXTENSION_PRIORITY_LOWER,
parseHTML() {
return [
{
tag: 'code',
preserveWhitespace: true,
getContent(element, schema) {
return Fragment.from(schema.text(element.textContent));
},
},
];
},
addKeyboardShortcuts() {
return {
ArrowRight: () => {

View File

@ -1,5 +1,6 @@
import { lowlight } from 'lowlight/lib/core';
import { textblockTypeInputRule } from '@tiptap/core';
import { Fragment } from '@tiptap/pm/model';
import { base64DecodeUnicode } from '~/lib/utils/text_utility';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import languageLoader from '../services/code_block_language_loader';
@ -49,8 +50,7 @@ export default CodeBlockHighlight.extend({
const source = base64DecodeUnicode(
element.dataset.diagramSrc.replace('data:text/plain;base64,', ''),
);
const node = schema.node('paragraph', {}, [schema.text(source)]);
return node.content;
return Fragment.from(schema.text(source));
},
},
];

View File

@ -24,7 +24,6 @@ const code = {
close: generateCodeTag(closeTag),
mixable: true,
escape: false,
expelEnclosingWhitespace: true,
};
export default code;

View File

@ -42,7 +42,7 @@ export default {
<template #default="{ mergeRequests, count, hasNextPage, loadMore, loading, error }">
<collapsible-section :count="count" :loading="loading || error" :title="list.title">
<div>
<div class="gl-overflow-x-scroll">
<div class="gl-overflow-x-auto">
<table class="gl-w-full">
<colgroup>
<col style="width: 60px" />

View File

@ -106,20 +106,20 @@ export default {
<assigned-users :users="mergeRequest.reviewers.nodes" type="REVIEWERS" />
</td>
<td class="gl-py-4 gl-pl-3 gl-pr-5 gl-align-top">
<div class="gl-flex gl-justify-end" :aria-label="statsAriaLabel">
<div class="gl-flex gl-justify-end gl-gap-3" :aria-label="statsAriaLabel">
<div class="gl-whitespace-nowrap">
<gl-icon name="comments" class="!gl-align-middle" />
{{ mergeRequest.userNotesCount }}
</div>
<div class="gl-ml-5 gl-whitespace-nowrap">
<div class="gl-whitespace-nowrap">
<gl-icon name="doc-code" />
<span>{{ mergeRequest.diffStatsSummary.fileCount }}</span>
</div>
<div class="gl-ml-3 gl-flex gl-items-center gl-font-bold gl-text-green-600">
<div class="gl-flex gl-items-center gl-font-bold gl-text-green-600">
<span>+</span>
<span>{{ mergeRequest.diffStatsSummary.additions }}</span>
</div>
<div class="gl-ml-3 gl-flex gl-items-center gl-font-bold gl-text-red-500">
<div class="gl-flex gl-items-center gl-font-bold gl-text-red-500">
<span></span>
<span>{{ mergeRequest.diffStatsSummary.deletions }}</span>
</div>

View File

@ -1,5 +1,6 @@
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { localeDateFormat, isValidDate } from '~/lib/utils/datetime_utility';
import {
NEXT_CLEANUP_LABEL,
NOT_SCHEDULED_POLICY_TEXT,
@ -24,7 +25,11 @@ export default {
},
computed: {
parsedValue() {
return this.enabled ? this.value : NOT_SCHEDULED_POLICY_TEXT;
const date = new Date(this.value);
const isValid = isValidDate(date);
return this.enabled && isValid
? localeDateFormat.asDateTimeFull.format(date)
: NOT_SCHEDULED_POLICY_TEXT;
},
},
i18n: {

View File

@ -72,6 +72,9 @@ export default {
const urlPart = joinPaths(gon.relative_url_root || '', `/${this.fullPath}`, `/-/commits/`);
return urlPart;
},
isLoading() {
return this.$apollo.queries.project.loading;
},
},
methods: {
async fetchContainingRefs({ query, namespace }) {
@ -109,27 +112,29 @@ export default {
<template>
<div class="gl-border-t">
<refs-list
v-if="hasBranches"
:has-containing-refs="hasContainingBranches"
:is-loading="$apollo.queries.project.loading"
:tipping-refs="tippingBranches"
:containing-refs="containingBranches"
:namespace="$options.i18n.branches"
:url-part="commitsUrlPart"
:ref-type="$options.BRANCHES_REF_TYPE"
@[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingBranches"
/>
<refs-list
v-if="hasTags"
:has-containing-refs="hasContainingTags"
:is-loading="$apollo.queries.project.loading"
:tipping-refs="tippingTags"
:containing-refs="containingTags"
:namespace="$options.i18n.tags"
:url-part="commitsUrlPart"
:ref-type="$options.TAGS_REF_TYPE"
@[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingTags"
/>
<div class="well-segment">
<refs-list
:has-containing-refs="hasContainingBranches"
:is-loading="isLoading"
:tipping-refs="tippingBranches"
:containing-refs="containingBranches"
:namespace="$options.i18n.branches"
:url-part="commitsUrlPart"
:ref-type="$options.BRANCHES_REF_TYPE"
@[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingBranches"
/>
</div>
<div class="well-segment">
<refs-list
:has-containing-refs="hasContainingTags"
:is-loading="isLoading"
:tipping-refs="tippingTags"
:containing-refs="containingTags"
:namespace="$options.i18n.tags"
:url-part="commitsUrlPart"
:ref-type="$options.TAGS_REF_TYPE"
@[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingTags"
/>
</div>
</div>
</template>

View File

@ -1,6 +1,12 @@
<script>
import { GlCollapse, GlBadge, GlButton, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { CONTAINING_COMMIT, FETCH_CONTAINING_REFS_EVENT, BRANCHES_REF_TYPE } from '../constants';
import { GlCollapse, GlBadge, GlButton, GlIcon, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui';
import {
CONTAINING_COMMIT,
FETCH_CONTAINING_REFS_EVENT,
BRANCHES_REF_TYPE,
EMPTY_BRANCHES_MESSAGE,
EMPTY_TAGS_MESSAGE,
} from '../constants';
export default {
name: 'RefsList',
@ -10,6 +16,7 @@ export default {
GlBadge,
GlButton,
GlIcon,
GlLoadingIcon,
},
props: {
urlPart: {
@ -58,6 +65,15 @@ export default {
refIcon() {
return this.refType === BRANCHES_REF_TYPE ? 'branch' : 'tag';
},
showEmptyMessage() {
return this.tippingRefs.length === 0 && this.containingRefs.length === 0 && !this.isLoading;
},
showNameSpace() {
return (this.tippingRefs.length !== 0 || this.containingRefs.length !== 0) && !this.isLoading;
},
emptyMessage() {
return this.refType === BRANCHES_REF_TYPE ? EMPTY_BRANCHES_MESSAGE : EMPTY_TAGS_MESSAGE;
},
},
methods: {
toggleCollapse() {
@ -78,9 +94,11 @@ export default {
</script>
<template>
<div class="well-segment">
<gl-icon :name="refIcon" class="gl-ml-2 gl-mr-3" />
<span data-testid="title" class="gl-mr-2">{{ namespace }}</span>
<div>
<gl-icon :name="refIcon" :size="16" class="gl-ml-2 gl-mr-3" />
<gl-loading-icon v-if="isLoading" size="sm" inline />
<span v-if="showEmptyMessage">{{ emptyMessage }}</span>
<span v-else-if="showNameSpace" data-testid="title" class="gl-mr-2">{{ namespace }}</span>
<gl-badge
v-for="ref in tippingRefs"
:key="ref"

View File

@ -21,3 +21,6 @@ export const FETCH_COMMIT_REFERENCES_ERROR = s__(
export const BRANCHES_REF_TYPE = 'heads';
export const TAGS_REF_TYPE = 'tags';
export const EMPTY_BRANCHES_MESSAGE = __('No related branches found');
export const EMPTY_TAGS_MESSAGE = __('No related tags found');

View File

@ -1,9 +1,8 @@
-# TODO: add fork suggestion (commits only)
%diff-file{ data: web_component_context }
.rd-diff-file{ id: id, data: server_data }
%diff-file{ id: id, data: server_data }
.rd-diff-file
= render RapidDiffs::DiffFileHeaderComponent.new(diff_file: @diff_file)
.rd-diff-file-body
-# TODO: replace with ViewComponents for each viewer
= render 'projects/diffs/viewer', viewer: viewer
= render viewer_component.new(diff_file: @diff_file)
%diff-file-mounted

View File

@ -17,23 +17,26 @@ module RapidDiffs
project = @diff_file.repository.project
params = tree_join(@diff_file.content_sha, @diff_file.file_path)
{
viewer: viewer_component.viewer_name,
blob_diff_path: project_blob_diff_path(project, params)
}
end
def web_component_context
viewer_name = viewer.partial_name
if viewer_name == 'text'
viewer_name = @parallel_view ? 'text_parallel' : 'text_inline'
end
def viewer_component
# return Viewers::CollapsedComponent if collapsed?
# return Viewers::NotDiffableComponent unless diffable?
{
viewer: viewer_name
}
end
is_text = @diff_file.text_diff?
return Viewers::Text::ParallelViewComponent if is_text && @parallel_view
return Viewers::Text::InlineViewComponent if is_text
return Viewers::NoPreviewComponent if @diff_file.content_changed?
def viewer
@diff_file.view_component_viewer
# return Viewers::AddedComponent if new_file?
# return Viewers::DeletedComponent if deleted_file?
# return Viewers::RenamedComponent if renamed_file?
# return Viewers::ModeChangedComponent if mode_changed?
Viewers::NoPreviewComponent
end
end
end

View File

@ -12,7 +12,7 @@
-# * toggle file comments
-# * submodule compare
.rd-diff-file-header
.rd-diff-file-header{ data: { testid: 'rd-diff-file-header' } }
.rd-diff-file-title
- if @diff_file.submodule?
%span

View File

@ -0,0 +1 @@
= _("No preview for this file type")

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
class NoPreviewComponent < ViewerComponent
def self.viewer_name
'no_preview'
end
end
end
end

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
class DiffHunkComponent < ViewComponent::Base
include ::Gitlab::Utils::StrongMemoize
MAX_EXPANDABLE_LINES = 20
def initialize(diff_hunk:, diff_file:)
@diff_hunk = diff_hunk
@diff_file = diff_file
end
def line_content(line)
if line.blank?
""
else
# `sub` and substring-ing would destroy HTML-safeness of `line`
line[1, line.length]
end
end
def line_text(line)
return unless line
line.rich_text ? line_content(line.rich_text) : line.text
end
def line_link(line, position)
return [] unless line && !line.meta?
line_number = position == :new ? line.new_pos : line.old_pos
id = @diff_file.line_side_code(line, position)
link = link_to line_number, "##{id}", { data: { line_number: line_number } }
[link, id]
end
def legacy_id(line)
return unless line
@diff_file.line_code(line)
end
def header_text
@diff_hunk[:header].text
end
def expand_buttons
return render ExpandLinesComponent.new(direction: :both) if show_expand_both?
buttons = ''
buttons += render ExpandLinesComponent.new(direction: :down) if show_expand_down?
buttons += render ExpandLinesComponent.new(direction: :up) if show_expand_up?
buttons
end
strong_memoize_attr :expand_buttons
private
def line_count_between
raise NotImplementedError
end
def show_expand_both?
line_count_between != 0 && line_count_between < MAX_EXPANDABLE_LINES
end
def show_expand_down?
@diff_hunk[:lines].empty? || @diff_hunk[:prev]
end
def show_expand_up?
!@diff_hunk[:header]&.index.nil?
end
end
end
end
end

View File

@ -0,0 +1,2 @@
%button
= helpers.sprite_icon(icon_name)

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
ICON_NAMES = {
up: 'expand-up',
down: 'expand-down',
both: 'expand'
}.freeze
class ExpandLinesComponent < ViewComponent::Base
def initialize(direction:)
@direction = direction
end
def icon_name
ICON_NAMES[@direction]
end
end
end
end
end

View File

@ -0,0 +1,13 @@
- if @diff_hunk[:header]
%tr
%td{ colspan: '2' }= expand_buttons.html_safe
%td= header_text
- @diff_hunk[:lines].each do |line|
%tr
- legacy_line_id = legacy_id(line)
- link, id = line_link(line, :old)
%td{ id: id, data: { legacy_id: legacy_line_id } }= link
- link, id = line_link(line, :new)
%td{ id: id, data: { legacy_id: legacy_line_id } }= link
%td= line_text(line)

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
class InlineHunkComponent < DiffHunkComponent
def line_count_between
prev = @diff_hunk[:prev]
return 0 if !prev || @diff_hunk[:lines].empty? || prev[:lines].empty?
@diff_hunk[:lines].first.old_pos - prev[:lines].last.old_pos
end
end
end
end
end

View File

@ -0,0 +1 @@
= render_parent

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
class InlineViewComponent < TextViewComponent
def self.viewer_name
'text_inline'
end
def lines
@diff_file.diff_lines_with_match_tail
end
def diff_line(line)
line
end
def hunk_view_component
InlineHunkComponent
end
def column_titles
[
s_('RapidDiffs|Original line number'),
s_('RapidDiffs|Diff line number'),
s_('RapidDiffs|Diff line')
]
end
end
end
end
end

View File

@ -0,0 +1,13 @@
- if @diff_hunk[:header]
%tr
- 2.times do
%td= expand_buttons.html_safe
%td= header_text
- @diff_hunk[:lines].each do |pair|
%tr
- [pair[:left], pair[:right]].each_with_index do |line, index|
- position = index == 0 ? :old : :new
- link, id = line_link(line, position)
%td{ id: id, data: { legacy_id: legacy_id(line) } }= link
%td= line_text(line)

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
class ParallelHunkComponent < DiffHunkComponent
def line_count_between
prev = @diff_hunk[:prev]
return 0 if !prev || @diff_hunk[:lines].empty? || prev[:lines].empty?
first_pair = @diff_hunk[:lines].first
first_line = first_pair[:left] || first_pair[:right]
prev_pair = prev[:lines].last
prev_line = prev_pair[:left] || prev_pair[:right]
first_line.old_pos - prev_line.old_pos
end
end
end
end
end

View File

@ -0,0 +1 @@
= render_parent

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
class ParallelViewComponent < TextViewComponent
def self.viewer_name
'text_parallel'
end
def lines
@diff_file.parallel_diff_lines_with_match_tail
end
# we need to iterate over diff lines to create an array of diff hunks
# because parallel diffs can have empty sides we need to provide a line from a side that is not empty
def diff_line(line)
line[:left] || line[:right]
end
def hunk_view_component
ParallelHunkComponent
end
def column_titles
[
s_('RapidDiffs|Original line number'),
s_('RapidDiffs|Original line'),
s_('RapidDiffs|Diff line number'),
s_('RapidDiffs|Diff line')
]
end
end
end
end
end

View File

@ -0,0 +1,8 @@
%table
%thead.gl-sr-only
%tr
- column_titles.each do |title|
%th= title
%tbody
- diff_hunks.each do |diff_hunk|
= render hunk_view(diff_hunk)

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
module Text
class TextViewComponent < ViewerComponent
def lines
raise NotImplementedError
end
def diff_line(line)
raise NotImplementedError
end
def hunk_view_component
raise NotImplementedError
end
def column_titles
raise NotImplementedError
end
def hunk_view(diff_hunk)
hunk_view_component.new(
diff_hunk: diff_hunk,
diff_file: @diff_file
)
end
def diff_hunks
return [] if lines.empty?
hunks = []
current_hunk = nil
lines.each do |line|
current_line = diff_line(line)
is_match = current_line.type == 'match'
if is_match || current_hunk.nil?
current_hunk = create_hunk(hunks.last, current_hunk, current_line, is_match, line)
hunks << current_hunk
else
current_hunk[:lines] << line
end
end
hunks
end
private
def create_hunk(prev, current_hunk, current_line, is_match, line)
new_hunk = {
header: is_match ? current_line : nil,
lines: is_match ? [] : [line],
prev: prev
}
current_hunk[:next] = new_hunk if current_hunk
new_hunk
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module RapidDiffs
module Viewers
class ViewerComponent < ViewComponent::Base
def self.viewer_name
raise NotImplementedError
end
def initialize(diff_file:)
@diff_file = diff_file
end
end
end
end

View File

@ -601,16 +601,6 @@ module Ci
end
end
# TODO Remove in 16.0 when runners are known to send a system_id
# For now, heartbeats with version updates might result in two Sidekiq jobs being queued if a runner has a system_id
# This is not a problem since the jobs are deduplicated on the version
def schedule_runner_version_update(new_version)
return if Feature.enabled?(:hide_duplicate_runner_manager_fields_in_runner)
return unless new_version && Gitlab::Ci::RunnerReleases.instance.enabled?
Ci::Runners::ProcessRunnerVersionUpdateWorker.perform_async(new_version)
end
def prefix_for_new_and_legacy_runner
return if registration_token_registration_type?

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
module Ci
class CollectPipelineAnalyticsService
TIME_BUCKETS_LIMIT = 1.week.in_hours + 1 # +1 to add some error margin
STATUS_GROUP_TO_STATUSES = { success: %w[success], failed: %w[failed], other: %w[canceled skipped] }.freeze
STATUS_GROUPS = STATUS_GROUP_TO_STATUSES.keys.freeze
STATUS_TO_STATUS_GROUP = STATUS_GROUP_TO_STATUSES.flat_map { |k, v| v.product([k]) }.to_h
def initialize(current_user:, project:, from_time:, to_time:, status_groups: [:all])
@current_user = current_user
@project = project
@status_groups = status_groups
@from_time = from_time || 1.week.ago.utc
@to_time = to_time || Time.now.utc
end
def execute
return ServiceResponse.error(message: 'Project must be specified') unless @project
unless ::Gitlab::ClickHouse.configured?
return ServiceResponse.error(message: 'ClickHouse database is not configured')
end
return ServiceResponse.error(message: 'Not allowed') unless allowed?
if (@to_time - @from_time) / 1.hour > TIME_BUCKETS_LIMIT
return ServiceResponse.error(message: "Maximum of #{TIME_BUCKETS_LIMIT} 1-hour intervals can be requested")
end
ServiceResponse.success(payload: { aggregate: calculate_aggregate })
end
private
def allowed?
@current_user&.can?(:read_ci_cd_analytics, @project)
end
def clickhouse_model
::ClickHouse::Models::Ci::FinishedPipelinesHourly
end
def calculate_aggregate
result = @status_groups.index_with(0)
query = clickhouse_model.for_project(@project).within_dates(@from_time, @to_time)
if @status_groups.include?(:all)
all_query = query.select(query.count_pipelines_function.as('all'))
result[:all] = ::ClickHouse::Client.select(all_query.to_sql, :main).first['all']
end
if @status_groups.intersect?(STATUS_GROUPS)
query = query
.select(:status, query.count_pipelines_function.as('count'))
.by_status(@status_groups.flat_map(&STATUS_GROUP_TO_STATUSES).compact)
.group_by_status
result_by_status = ::ClickHouse::Client.select(query.to_sql, :main).map(&:values).to_h
result_by_status.each_pair { |status, count| result[STATUS_TO_STATUS_GROUP[status]] += count }
end
result
end
end
end

View File

@ -1,5 +1,6 @@
= universal_stylesheet_link_tag 'application'
= universal_stylesheet_link_tag 'application_utilities'
= universal_stylesheet_link_tag 'tailwind'
= universal_stylesheet_link_tag 'lookbook/rapid_diffs'
= webpack_bundle_tag 'javascripts/entrypoints/lookbook/rapid_diffs'

View File

@ -38,7 +38,13 @@
%span.gl-text-primary= n_('parent', 'parents', @commit.parents.count)
- @commit.parents.each do |parent|
= link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha"
#js-commit-branches-and-tags{ data: { full_path: @project.full_path, commit_sha: @commit.id } }
#js-commit-branches-and-tags.gl-border-t{ data: { full_path: @project.full_path, commit_sha: @commit.short_id } }
.well-segment
= sprite_icon('branch', css_class: "gl-ml-2 gl-mr-3")
= gl_loading_icon(inline: true, css_class: 'gl-align-middle')
.well-segment
= sprite_icon('tag', css_class: "gl-ml-2 gl-mr-3")
= gl_loading_icon(inline: true, css_class: 'gl-align-middle')
.well-segment.merge-request-info
.icon-container

View File

@ -49,7 +49,7 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
namespace: member.source
}
with_context(context) do
Members::DestroyService.new.execute(member, skip_authorization: true)
Members::DestroyService.new.execute(member, skip_authorization: true, skip_subresources: true)
expired_user = member.user

View File

@ -5,3 +5,5 @@ feature_category: software_composition_analysis
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151444
milestone: '17.0'
queued_migration_version: 20240425205205
finalize_after: '2024-05-06'
finalized_by: 20240909204952

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class FinalizeOsSbomComponentNormalizationMigration < Gitlab::Database::Migration[2.2]
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '17.4'
MIGRATION_NAME = 'RemoveNamespaceFromOsTypeSbomComponents'
disable_ddl_transaction!
def up
ensure_batched_background_migration_is_finished(
job_class_name: MIGRATION_NAME,
table_name: :sbom_components,
column_name: :id,
job_arguments: [],
finalize: true
)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
8ca259a8eae0ae780f477b4ecf01ee29a0de5a3b053950b4f14dad0e80acba98

View File

@ -212,14 +212,13 @@ This list of limitations only reflects the latest version of GitLab. If you are
[GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit) Terraform and Ansible scripts to deploy and operate production
GitLab instances based on our [Reference Architectures](../reference_architectures/index.md), including automation of common daily tasks.
[Epic 1465](https://gitlab.com/groups/gitlab-org/-/epics/1465) proposes to improve Geo installation even more.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** site.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on **secondary** sites where [http proxying is disabled](secondary_proxy/index.md#disable-secondary-site-http-proxying).
- [Selective synchronization](replication/selective_synchronization.md) only limits what repositories and files are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accommodate compliance / export control use cases.
- [Pages access control](../../user/project/pages/pages_access_control.md) doesn't work on secondaries. See [GitLab issue #9336](https://gitlab.com/gitlab-org/gitlab/-/issues/9336) for details.
- [Disaster recovery](disaster_recovery/index.md) for deployments that have multiple secondary sites causes downtime due to the need to perform complete re-synchronization and re-configuration of all non-promoted secondaries to follow the new primary site.
- [Disaster recovery](disaster_recovery/index.md) for deployments that have multiple secondary sites causes downtime due to the need to re-initialize PostgreSQL streaming replication on all non-promoted secondaries to follow the new primary site.
- For Git over SSH, to make the project clone URL display correctly regardless of which site you are browsing, secondary sites must use the same port as the primary. [GitLab issue #339262](https://gitlab.com/gitlab-org/gitlab/-/issues/339262) proposes to remove this limitation.
- Git push over SSH against a secondary site does not work for pushes over 1.86 GB. [GitLab issue #413109](https://gitlab.com/gitlab-org/gitlab/-/issues/413109) tracks this bug.
- Backups [cannot be run on secondaries](replication/troubleshooting/replication.md#message-error-canceling-statement-due-to-conflict-with-recovery).
- Git clone and fetch requests with option `--depth` over SSH against a secondary site does not work and hangs indefinitely if the secondary site is not up to date at the time the request is initiated. For more information, see [issue 391980](https://gitlab.com/gitlab-org/gitlab/-/issues/391980).
- Backups [cannot be run on Geo secondary sites](replication/troubleshooting/replication.md#message-error-canceling-statement-due-to-conflict-with-recovery).
- Git push with options over SSH against a secondary site does not work and terminates the connection. For more information, see [issue 417186](https://gitlab.com/gitlab-org/gitlab/-/issues/417186).
- The Geo secondary site does not accelerate (serve) the clone request for the first stage of the pipeline in most cases. Later stages are not guaranteed to be served by the secondary site either, for example if the Git change is large, bandwidth is small, or pipeline stages are short. In general, it does serve the clone request for subsequent stages. [Issue 446176](https://gitlab.com/gitlab-org/gitlab/-/issues/446176) discusses the reasons for this and proposes an enhancement to increase the chance that Runner clone requests are served from the secondary site.
- When a single Git repository receives pushes at a high-enough rate, the secondary site's local copy can be perpetually out-of-date. This causes all Git fetches of that repository to be forwarded to the primary site. See [GitLab issue #455870](https://gitlab.com/gitlab-org/gitlab/-/issues/455870).
@ -228,6 +227,8 @@ This list of limitations only reflects the latest version of GitLab. If you are
- GitLab Pages - should always use a separate domain, as part of [the prerequisites for running GitLab Pages](../pages/index.md#prerequisites).
- With a [unified URL](secondary_proxy/index.md#set-up-a-unified-url-for-geo-sites), Let's Encrypt can't generate certificates unless it can reach both IPs through the same domain. To use TLS certificates with Let's Encrypt, you can manually point the domain to one of the Geo sites, generate the certificate, then copy it to all other sites.
- When a [secondary site uses a separate URL](secondary_proxy/index.md#set-up-a-separate-url-for-a-secondary-geo-site) from the primary site, [signing in the secondary site using SAML](replication/single_sign_on.md#saml-with-separate-url-with-proxying-enabled) is only supported if the SAML Identity Provider (IdP) allows an application to be configured with multiple callback URLs.
- Git clone and fetch requests with option `--depth` over SSH against a secondary site does not work and hangs indefinitely if the secondary site is not up to date at the time the request is initiated. This is due to problems related to translating Git SSH to Git https during proxying. For more information, see [issue 391980](https://gitlab.com/gitlab-org/gitlab/-/issues/391980). A new workflow that does not involve the aforementioned translation step is now available for Linux-packaged GitLab Geo secondary sites which can be enabled with a feature flag. For more details, see [comment in issue 454707](https://gitlab.com/gitlab-org/gitlab/-/issues/454707#note_2102067451). The fix for Cloud Native GitLab Geo secondary sites is tracked in [issue 5641](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/5641).
- Some customers have reported that `git fetch` over SSH when the secondary site is out of date hangs and/or times out and fails. `git clone` requests over SSH are not impacted. For more information, see [issue 454707](https://gitlab.com/gitlab-org/gitlab/-/issues/454707). A fix available for Linux-packaged GitLab Geo secondary sites which can be enabled with a feature flag. For more details, see [comment in issue 454707](https://gitlab.com/gitlab-org/gitlab/-/issues/454707#note_2102067451). The fix for Cloud Native GitLab Geo secondary sites is tracked in [issue 5641](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/5641).
### Limitations on replication/verification

View File

@ -156,7 +156,6 @@ You can keep track of the progress to implement the missing items in
these epics/issues:
- [Geo: Improve the self-service Geo replication framework](https://gitlab.com/groups/gitlab-org/-/epics/3761)
- [Geo: Move existing blobs to framework](https://gitlab.com/groups/gitlab-org/-/epics/3588)
### Replicated data types behind a feature flag

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -64,7 +64,7 @@ would be helpful if one can have a shell to aid debugging. When a job is
running, on the right panel you can see a button `debug` that opens the terminal
for the current job. Only the person who started a job can debug it.
![Example of job running with terminal available](img/interactive_web_terminal_running_job.png)
![Example of job running with terminal available](img/interactive_web_terminal_running_job_v17_3.png)
When selected, a new tab opens to the terminal page where you can access
the terminal and type commands like in a standard shell.

View File

@ -694,67 +694,3 @@ Example response:
}
}
```
### Upcoming Reconciliations (deprecated)
These endpoints have been replaced with the ones in the [Internal Upcoming Reconciliations API](#upcoming-reconciliations).
### Update `upcoming_reconciliations`
Use a PUT command to update `upcoming_reconciliations`.
```plaintext
PUT /internal/upcoming_reconciliations
```
| Attribute | Type | Required | Description |
|:-------------------|:-----------|:---------|:------------|
| `upcoming_reconciliations` | array | yes | Array of upcoming reconciliations |
Each array element contains:
| Attribute | Type | Required | Description |
|:-------------------|:-----------|:---------|:------------|
| `namespace_id` | integer | yes | ID of the namespace to be reconciled |
| `next_reconciliation_date` | date | yes | Date of the next reconciliation |
| `display_alert_from` | date | yes | Start date to display alert of upcoming reconciliation |
Example request:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <admin_access_token>" --header "Content-Type: application/json" \
--data '{"upcoming_reconciliations": [{"namespace_id": 127, "next_reconciliation_date": "13 Jun 2021", "display_alert_from": "06 Jun 2021"}, {"namespace_id": 129, "next_reconciliation_date": "12 Jun 2021", "display_alert_from": "05 Jun 2021"}]}' \
"https://gitlab.com/api/v4/internal/upcoming_reconciliations"
```
Example response:
```plaintext
200
```
### Delete an `upcoming_reconciliation`
Use a DELETE command to delete an `upcoming_reconciliation`.
```plaintext
DELETE /internal/upcoming_reconciliations
```
| Attribute | Type | Required | Description |
|:---------------|:--------|:---------|:----------------------------------------------------------------------------------|
| `namespace_id` | integer | yes | The ID of the GitLab.com namespace that no longer has an upcoming reconciliation. |
Example request:
```shell
curl --request DELETE \
--url "http://localhost:3000/api/v4/internal/upcoming_reconciliations?namespace_id=22" \
--header 'PRIVATE-TOKEN: <admin_access_token>'
```
Example response:
```plaintext
204
```

View File

@ -11,7 +11,7 @@ DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
This page contains information about the minimum system requirements to install GitLab.
This page contains information about the system requirements to install GitLab.
## Hardware
@ -45,8 +45,9 @@ For a maximum of 20 requests per second or 1,000 users, you should have 16 GB of
For more users or higher workload,
see [reference architectures](../administration/reference_architectures/index.md).
In certain circumstances, GitLab might run in a
[memory-constrained environment](https://docs.gitlab.com/omnibus/settings/memory_constrained_envs.html).
In some cases, GitLab can run with at least 8 GB of memory.
For more information, see
[running GitLab in a memory-constrained environment](https://docs.gitlab.com/omnibus/settings/memory_constrained_envs.html).
## Database

View File

@ -329,6 +329,7 @@ You can sort members by **Account**, **Access granted**, **Role**, or **Last sig
## Add users to a group
> - Expiring access email notification [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12704) in GitLab 16.2.
> - Access expiration date for direct members of subgroups and projects [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/471051) in GitLab 17.4.
You can give a user access to all projects in a group.

View File

@ -72,6 +72,7 @@ In the above example:
## Add users to a project
> - Expiring access email notification [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12704) in GitLab 16.2.
> - Access expiration date for direct members of subgroups and projects [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/471051) in GitLab 17.4.
Add users to a project so they become direct members and have permission
to perform actions.

View File

@ -20,6 +20,18 @@ module ClickHouse # rubocop:disable Gitlab/BoundedContexts -- Existing module
where(path: project.project_namespace.traversal_path)
end
def within_dates(from_time, to_time)
query = self
started_at_bucket = @query_builder.table[:started_at_bucket]
# rubocop: disable CodeReuse/ActiveRecord -- this is a ClickHouse model
query = query.where(started_at_bucket.gteq(format_time(from_time))) if from_time
query = query.where(started_at_bucket.lt(format_time(to_time))) if to_time
# rubocop: enable CodeReuse/ActiveRecord
query
end
def by_status(statuses)
where(status: statuses)
end
@ -31,6 +43,16 @@ module ClickHouse # rubocop:disable Gitlab/BoundedContexts -- Existing module
def count_pipelines_function
Arel::Nodes::NamedFunction.new('countMerge', [@query_builder.table[:count_pipelines]])
end
private
def format_time(date)
Arel::Nodes::NamedFunction.new('toDateTime64', [
Arel::Nodes::SqlLiteral.new(date.utc.strftime("'%Y-%m-%d %H:%M:%S'")),
6,
Arel::Nodes.build_quoted('UTC')
])
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Gitlab
module Backup
class JsonLogger < Gitlab::JsonLogger
exclude_context!
def self.file_name_noext
'backup_json'
end
end
end
end

View File

@ -1,39 +1,34 @@
# frozen_string_literal: true
module Gitlab
class BackupLogger < Gitlab::JsonLogger
exclude_context!
attr_reader :progress
class BackupLogger
attr_reader :progress, :json_logger
def initialize(progress)
@progress = progress
@json_logger = ::Gitlab::Backup::JsonLogger.build
end
def warn(message)
progress.puts Rainbow("#{Time.zone.now} -- #{message}").yellow
super
json_logger.warn(message: message)
end
def info(message)
progress.puts Rainbow("#{Time.zone.now} -- #{message}").cyan
super
json_logger.info(message: message)
end
def error(message)
progress.puts Rainbow("#{Time.zone.now} -- #{message}").red
super
json_logger.error(message: message)
end
def flush
progress.flush
end
def self.file_name_noext
'backup_json'
end
end
end

View File

@ -76,6 +76,14 @@ module Gitlab
Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
end
def line_side_code(line, position)
return if line.meta?
prefix = position == :old ? "L" : "R"
number = position == :old ? line.old_pos : line.new_pos
"line_#{file_hash}_#{prefix}#{number}"
end
def line_for_line_code(code)
diff_lines.find { |line| line_code(line) == code }
end
@ -266,6 +274,10 @@ module Gitlab
@parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
end
def parallel_diff_lines_with_match_tail
@parallel_diff_lines_with_match_tail ||= Gitlab::Diff::ParallelDiff.new(self).parallelize(diff_lines_with_match_tail)
end
def raw_diff
diff.diff.to_s
end
@ -289,6 +301,7 @@ module Gitlab
def file_hash
Digest::SHA1.hexdigest(file_path)
end
strong_memoize_attr :file_hash
def added_lines
strong_memoize(:added_lines) do
@ -387,11 +400,6 @@ module Gitlab
@rich_viewer = rich_viewer_class&.new(self)
end
# This is going to be updated with viewer components
def view_component_viewer
has_renderable? ? rendered.viewer : viewer
end
def alternate_viewer
alternate_viewer_class&.new(self)
end
@ -403,23 +411,29 @@ module Gitlab
# This adds the bottom match line to the array if needed. It contains
# the data to load more context lines.
def diff_lines_for_serializer
strong_memoize(:diff_lines_for_serializer) do
lines = highlighted_diff_lines
lines = diff_lines_with_match_tail
return if lines.empty?
next if lines.empty?
next if blob.nil?
last_line = lines.last
if last_line.new_pos < total_blob_lines(blob) && !deleted_file?
match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
lines.push(match_line)
end
lines
end
lines
end
def diff_lines_with_match_tail
lines = highlighted_diff_lines
return [] if lines.empty?
return [] if blob.nil?
last_line = lines.last
if last_line.new_pos < total_blob_lines(blob) && !deleted_file?
match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
lines.push(match_line)
end
lines
end
strong_memoize_attr(:diff_lines_with_match_tail)
def fully_expanded?
return true if binary?
@ -449,6 +463,10 @@ module Gitlab
diffable? && !deleted_file?
end
def text_diff?
modified_file? && text?
end
private
def diffable_by_attribute?

View File

@ -9,13 +9,12 @@ module Gitlab
@diff_file = diff_file
end
def parallelize
def parallelize(diff_lines = diff_file.highlighted_diff_lines)
i = 0
free_right_index = nil
lines = []
highlighted_diff_lines = diff_file.highlighted_diff_lines
highlighted_diff_lines.each do |line|
diff_lines.each do |line|
if line.removed?
lines << {
left: line,

View File

@ -16276,6 +16276,9 @@ msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream has been successfully created."
msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream has been successfully saved."
msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream saved"
msgstr ""
@ -26363,9 +26366,6 @@ msgstr ""
msgid "GroupSelect|Select a group"
msgstr ""
msgid "GroupSettings| %{link_start}What do experiment and beta mean?%{link_end}"
msgstr ""
msgid "GroupSettings|'%{name}' has been scheduled for removal on %{date}."
msgstr ""
@ -26462,9 +26462,6 @@ msgstr ""
msgid "GroupSettings|Enable sending email notifications for this group and all its subgroups and projects"
msgstr ""
msgid "GroupSettings|Enabling these features is your acceptance of the %{link_start}GitLab Testing Agreement%{link_end}."
msgstr ""
msgid "GroupSettings|Enforce SSH Certificates"
msgstr ""
@ -26576,9 +26573,6 @@ msgstr ""
msgid "GroupSettings|There was a problem updating the group CI/CD settings: %{error_messages}."
msgstr ""
msgid "GroupSettings|These features are being developed and might be unstable."
msgstr ""
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr ""
@ -36153,6 +36147,12 @@ msgstr ""
msgid "No regions configured"
msgstr ""
msgid "No related branches found"
msgstr ""
msgid "No related tags found"
msgstr ""
msgid "No repository"
msgstr ""
@ -44652,6 +44652,18 @@ msgstr ""
msgid "Random"
msgstr ""
msgid "RapidDiffs|Diff line"
msgstr ""
msgid "RapidDiffs|Diff line number"
msgstr ""
msgid "RapidDiffs|Original line"
msgstr ""
msgid "RapidDiffs|Original line number"
msgstr ""
msgid "Rate Limits"
msgstr ""

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require "factory_bot"
module RapidDiffs
class DiffFileComponentPreview < ViewComponent::Preview
layout 'lookbook/rapid_diffs'
@ -9,7 +7,7 @@ module RapidDiffs
# @!group Code
def added_and_removed_lines
diff = "
hunk = "
--- a/app/views/layouts/preview/rapid_diffs.html.haml (revision eaba934a0bc6eed56cfd1f082e9fa3f5409f2938)
+++ b/app/views/layouts/preview/rapid_diffs.html.haml (date 1718119822001)
@@ -1,7 +1,6 @@
@ -23,11 +21,11 @@ module RapidDiffs
+%div{ style: 'padding: 20px', class: 'container-fluid' }
= yield
"
render(RapidDiffs::DiffFileComponent.new(diff_file: diff_file(diff)))
render(::RapidDiffs::DiffFileComponent.new(diff_file: diff_file(hunk)))
end
def added_lines
diff = "
hunk = "
--- a/app/views/layouts/preview/rapid_diffs.html.haml (revision eaba934a0bc6eed56cfd1f082e9fa3f5409f2938)
+++ b/app/views/layouts/preview/rapid_diffs.html.haml (date 1718118441569)
@@ -2,6 +2,7 @@
@ -39,11 +37,11 @@ module RapidDiffs
%div{ style: 'padding: 20px' }
= yield
"
render(RapidDiffs::DiffFileComponent.new(diff_file: diff_file(diff)))
render(::RapidDiffs::DiffFileComponent.new(diff_file: diff_file(hunk)))
end
def removed_lines
diff = "
hunk = "
--- a/app/views/layouts/preview/rapid_diffs.html.haml (revision eaba934a0bc6eed56cfd1f082e9fa3f5409f2938)
+++ b/app/views/layouts/preview/rapid_diffs.html.haml (date 1718119765262)
@@ -1,7 +1,6 @@
@ -55,11 +53,11 @@ module RapidDiffs
%div{ style: 'padding: 20px' }
= yield
"
render(RapidDiffs::DiffFileComponent.new(diff_file: diff_file(diff)))
render(::RapidDiffs::DiffFileComponent.new(diff_file: diff_file(hunk)))
end
def added_file
diff = "
hunk = "
--- /dev/null
+++ b/app/views/layouts/preview/rapid_diffs.html.haml (date 1718119765262)
@@ -0,0 +1,7 @@
@ -71,11 +69,11 @@ module RapidDiffs
+%div{ style: 'padding: 20px' }
+ = yield
"
render(RapidDiffs::DiffFileComponent.new(diff_file: diff_file(diff)))
render(::RapidDiffs::DiffFileComponent.new(diff_file: diff_file(hunk)))
end
def removed_file
diff = "
hunk = "
--- a/app/views/layouts/preview/rapid_diffs.html.haml (revision eaba934a0bc6eed56cfd1f082e9fa3f5409f2938)
+++ /dev/null
@@ -1,7 +1,0 @@
@ -87,29 +85,31 @@ module RapidDiffs
-%div{ style: 'padding: 20px' }
- = yield
"
render(RapidDiffs::DiffFileComponent.new(diff_file: diff_file(diff)))
render(::RapidDiffs::DiffFileComponent.new(diff_file: diff_file(hunk)))
end
# @!endgroup
private
def diff_content(diff)
diff.split("\n").filter_map(&:lstrip).reject(&:empty?).join("\n")
end
def diff_file(diff)
::Gitlab::Diff::File.new(raw_diff(diff_content(diff)), repository: FakeRepository.new).tap do |file|
file.instance_variable_set(:@new_blob, Blob.decorate(raw_blob(diff_content(diff))))
def diff_file(hunk)
diff = raw_diff(diff_content(hunk))
diff_refs = ::Gitlab::Diff::DiffRefs.new(base_sha: 'a', head_sha: 'b')
::Gitlab::Diff::File.new(diff, repository: FakeRepository.new, diff_refs: diff_refs).tap do |file|
file.instance_variable_set(:@new_blob, Blob.decorate(raw_blob(diff_content(hunk))))
end
end
def raw_diff(diff)
Gitlab::Git::Diff.new(
def diff_content(hunk)
hunk.split("\n").filter_map(&:lstrip).reject(&:empty?).join("\n")
end
def raw_diff(hunk)
::Gitlab::Git::Diff.new(
{
diff: diff,
new_path: new_path(diff),
old_path: old_path(diff),
diff: hunk,
new_path: new_path(hunk),
old_path: old_path(hunk),
a_mode: '0',
b_mode: '100644',
new_file: true,
@ -119,24 +119,24 @@ module RapidDiffs
})
end
def raw_blob(diff)
Gitlab::Git::Blob.new(
def raw_blob(hunk)
::Gitlab::Git::Blob.new(
id: 'bba46076dd3e6a406b45ad98ef3b8194fde8b568',
commit_id: 'master',
size: 264,
name: new_path(diff),
path: new_path(diff),
name: new_path(hunk),
path: new_path(hunk),
data: "",
mode: '100644'
)
end
def old_path(diff)
diff[%r{--- a/([^\s\n]*)}, 1]
def old_path(hunk)
hunk[%r{--- a/([^\s\n]*)}, 1]
end
def new_path(diff)
diff[%r{\+\+\+ b/([^\s\n]*)}, 1]
def new_path(hunk)
hunk[%r{\+\+\+ b/([^\s\n]*)}, 1]
end
class FakeRepository

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::DiffFileComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
let(:repository) { diff_file.repository }
let(:project) { repository.container }
let(:namespace) { project.namespace }
let(:web_component) { page.find('diff-file') }
it "renders" do
render_component
expect(page).to have_selector('diff-file')
expect(page).to have_selector('diff-file-mounted')
end
it "renders server data" do
render_component
diff_path = "/#{namespace.to_param}/#{project.to_param}/-/blob/#{diff_file.content_sha}/#{diff_file.file_path}/diff"
expect(web_component['data-blob-diff-path']).to eq(diff_path)
end
context "when is text diff" do
before do
allow(diff_file).to receive(:text_diff?).and_return(true)
end
it "renders parallel text viewer" do
render_component
expect(web_component['data-viewer']).to eq('text_inline')
end
it "renders parallel text viewer" do
render_component(parallel_view: true)
expect(web_component['data-viewer']).to eq('text_parallel')
end
end
context "when non-text content changed" do
before do
allow(diff_file).to receive(:text_diff?).and_return(false)
allow(diff_file).to receive(:content_changed?).and_return(true)
end
it "renders no preview" do
render_component
expect(web_component['data-viewer']).to eq('no_preview')
end
end
context "when no viewer found" do
before do
allow(diff_file).to receive(:text_diff?).and_return(false)
allow(diff_file).to receive(:content_changed?).and_return(false)
end
it "renders no preview" do
render_component
expect(web_component['data-viewer']).to eq('no_preview')
end
end
def render_component(**args)
render_inline(described_class.new(diff_file: diff_file, **args))
end
end

View File

@ -0,0 +1,62 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::DiffFileHeaderComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
let(:header) { page.find('[data-testid="rd-diff-file-header"]') }
it "renders file path" do
render_component
expect(header).to have_text(diff_file.file_path)
end
it "renders submodule info" do
allow(diff_file).to receive(:submodule?).and_return(true)
allow_next_instance_of(SubmoduleHelper) do |instance|
allow(instance).to receive(:submodule_links).and_return(nil)
end
render_component
expect(page.find('svg use')['href']).to include('folder-git')
expect(page).to have_text(diff_file.blob.name)
expect(page).to have_text(diff_file.blob.id[0..7])
end
it "renders path change" do
allow(diff_file).to receive(:renamed_file?).and_return(true)
allow(diff_file).to receive(:old_path).and_return('old/path')
allow(diff_file).to receive(:new_path).and_return('new/path')
render_component
expect(header).to have_text('old/path')
expect(header).to have_text('new/path')
end
it "renders mode change" do
allow(diff_file).to receive(:mode_changed?).and_return(true)
render_component
expect(header).to have_text("#{diff_file.a_mode}#{diff_file.b_mode}")
end
it "renders deleted message" do
allow(diff_file).to receive(:deleted_file?).and_return(true)
render_component
expect(header).to have_text('deleted')
end
it "renders LFS message" do
allow(diff_file).to receive(:stored_externally?).and_return(true)
allow(diff_file).to receive(:external_storage).and_return(:lfs)
render_component
expect(header).to have_text('LFS')
end
it "renders line count" do
render_component
expect(page.find('[data-testid="js-file-addition-line"]')).to have_text(diff_file.added_lines)
expect(page.find('[data-testid="js-file-deletion-line"]')).to have_text(diff_file.removed_lines)
end
def render_component
render_inline(described_class.new(diff_file: diff_file))
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::DiffHunkComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
describe '#expand_buttons' do
it { expect { create_component.expand_buttons }.to raise_error(NotImplementedError) }
end
def create_component
described_class.new(diff_file: diff_file, diff_hunk: { lines: [] })
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::ExpandLinesComponent, type: :component, feature_category: :code_review_workflow do
it "renders expand up" do
render_component(:up)
expect(page).to have_selector('button svg use[href$="#expand-up"]')
end
it "renders expand down" do
render_component(:down)
expect(page).to have_selector('button svg use[href$="#expand-down"]')
end
it "renders expand both" do
render_component(:both)
expect(page).to have_selector('button svg use[href$="#expand"]')
end
def render_component(direction)
render_inline(described_class.new(direction: direction))
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::InlineHunkComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
let(:lines) { diff_file.diff_lines_with_match_tail }
let(:hunk) do
{
header: lines.first,
lines: lines.drop(1)
}
end
it "renders header" do
render_component
expect(page).to have_text(hunk[:header].text)
end
it "renders lines" do
render_component
page_text = page.native.inner_html
hunk[:lines].each do |line|
text = line.rich_text || line.text
expect(page_text).to include(text.gsub(/^[\s+-]/, ''))
end
end
it "renders line id" do
old_line_id = diff_file.line_side_code(lines.second, :old)
new_line_id = diff_file.line_side_code(lines.second, :new)
render_component
expect(page).to have_selector("##{old_line_id}")
expect(page).to have_selector("##{new_line_id}")
end
it "renders line link" do
old_line_id = diff_file.line_side_code(lines.second, :old)
new_line_id = diff_file.line_side_code(lines.second, :new)
render_component
expect(page).to have_selector("a[href='##{old_line_id}']")
expect(page).to have_selector("a[href='##{new_line_id}']")
end
it "renders legacy line id" do
line_id = diff_file.line_code(lines.second)
render_component
expect(page).to have_selector("[data-legacy-id='#{line_id}']")
end
it "renders expand up" do
diff_hunk = {
header: Gitlab::Diff::Line.new("", 'match', 1, 0, 0),
lines: lines.drop(1)
}
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand-up"]')
end
it "renders expand down" do
diff_hunk = {
header: Gitlab::Diff::Line.new("", 'match', 100, 0, 0),
lines: []
}
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand-down"]')
end
it "renders both expand up and down" do
diff_hunk = {
header: Gitlab::Diff::Line.new("", 'match', 1, 0, 0),
lines: lines.drop(1),
prev: { lines: [] }
}
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand-up"]')
expect(page).to have_selector('button svg use[href$="#expand-down"]')
end
it "renders expand both" do
last_prev_line = lines.first
diff_hunk = {
header: lines.first,
lines: lines.drop(1),
prev: {
lines: [last_prev_line]
}
}
allow(diff_hunk[:lines].first).to receive(:old_pos).and_return(5)
allow(last_prev_line).to receive(:old_pos).and_return(2)
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand"]')
end
def render_component(diff_hunk = hunk)
render_inline(described_class.new(diff_file: diff_file, diff_hunk: diff_hunk))
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::InlineViewComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
let(:lines) { diff_file.diff_lines_with_match_tail }
it "renders inline lines" do
render_component
expect(page).to have_text(lines.first.rich_text)
end
it "renders headings" do
render_component
page_text = page.native.inner_html
headings = [
'Original line number',
'Diff line number',
'Diff line'
]
headings.each do |heading|
expect(page_text).to include(heading)
end
end
def render_component
render_inline(described_class.new(diff_file: diff_file))
end
end

View File

@ -0,0 +1,100 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::ParallelHunkComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
let(:lines) { diff_file.parallel_diff_lines_with_match_tail }
let(:hunk) do
{
header: lines.first[:left],
lines: lines.drop(1)
}
end
it "renders header" do
render_component
expect(page).to have_text(hunk[:header].text)
end
it "renders lines" do
render_component
page_text = page.native.inner_html
hunk[:lines].each do |line_side|
[line_side[:left], line_side[:right]].compact.each do |line|
text = line.rich_text || line.text
expect(page_text).to include(text.gsub(/^[\s+-]/, ''))
end
end
end
it "renders line id" do
old_line_id = diff_file.line_side_code(lines.second[:left], :old)
new_line_id = diff_file.line_side_code(lines.second[:right], :new)
render_component
expect(page).to have_selector("##{old_line_id}")
expect(page).to have_selector("##{new_line_id}")
end
it "renders line link" do
old_line_id = diff_file.line_side_code(lines.second[:left], :old)
new_line_id = diff_file.line_side_code(lines.second[:right], :new)
render_component
expect(page).to have_selector("a[href='##{old_line_id}']")
expect(page).to have_selector("a[href='##{new_line_id}']")
end
it "renders legacy line id" do
line_id = diff_file.line_code(lines.second[:left])
render_component
expect(page).to have_selector("[data-legacy-id='#{line_id}']")
end
it "renders expand up" do
diff_hunk = {
header: Gitlab::Diff::Line.new("", 'match', 1, 0, 0),
lines: lines.drop(1)
}
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand-up"]')
end
it "renders expand down" do
diff_hunk = {
header: Gitlab::Diff::Line.new("", 'match', 100, 0, 0),
lines: []
}
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand-down"]')
end
it "renders both expand up and down" do
diff_hunk = {
header: Gitlab::Diff::Line.new("", 'match', 1, 0, 0),
lines: lines.drop(1),
prev: { lines: [] }
}
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand-up"]')
expect(page).to have_selector('button svg use[href$="#expand-down"]')
end
it "renders expand both" do
last_prev_line = lines.first
diff_hunk = {
header: lines.first[:left],
lines: lines.drop(1),
prev: {
lines: [last_prev_line]
}
}
allow(diff_hunk[:lines].first[:left]).to receive(:old_pos).and_return(5)
allow(last_prev_line[:left]).to receive(:old_pos).and_return(2)
render_component(diff_hunk)
expect(page).to have_selector('button svg use[href$="#expand"]')
end
def render_component(diff_hunk = hunk)
render_inline(described_class.new(diff_file: diff_file, diff_hunk: diff_hunk))
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::ParallelViewComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
let(:lines) { diff_file.parallel_diff_lines_with_match_tail }
it "renders parallel lines" do
render_component
expect(page).to have_text(lines.first[:left].rich_text)
expect(page).to have_text(lines.first[:right].rich_text)
end
it "renders headings" do
render_component
page_text = page.native.inner_html
headings = [
'Original line number',
'Original line',
'Diff line number',
'Diff line'
]
headings.each do |heading|
expect(page_text).to include(heading)
end
end
def render_component
render_inline(described_class.new(diff_file: diff_file))
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::Text::TextViewComponent, type: :component, feature_category: :code_review_workflow do
let_it_be(:diff_file) { build(:diff_file) }
describe '#lines' do
it { delegates_implementation_for { create_component.lines } }
end
describe '#diff_line' do
it { delegates_implementation_for { create_component.diff_line(diff_file.highlighted_diff_lines.first) } }
end
describe '#hunk_view_component' do
it { delegates_implementation_for { create_component.hunk_view_component } }
end
describe '#column_titles' do
it { delegates_implementation_for { create_component.column_titles } }
end
def delegates_implementation_for
expect { yield }.to raise_error(NotImplementedError)
end
def create_component
described_class.new(diff_file: diff_file)
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe RapidDiffs::Viewers::ViewerComponent, type: :component, feature_category: :code_review_workflow do
describe '#viewer_name' do
it { expect { described_class.viewer_name }.to raise_error(NotImplementedError) }
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require_relative '../support/helpers/repo_helpers'
FactoryBot.define do
factory :diff_file, class: 'Gitlab::Diff::File' do
repository { create(:project, :repository).repository }
diff { repository.commit(RepoHelpers.sample_commit.id).raw_diffs.first }
diff_refs { repository.commit(RepoHelpers.sample_commit.id).diff_refs }
initialize_with do
new(
attributes[:diff],
diff_refs: attributes[:diff_refs],
repository: attributes[:repository]
)
end
end
end

View File

@ -694,7 +694,7 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]',
'`raise RuntimeError, "System commands must be given as an array of strings"`',
'` raise RuntimeError, "System commands must be given as an array of strings"`',
target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'
)
end
@ -773,11 +773,11 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
wait_for_requests
end
context 'selecting one word of text' do
context 'selecting one line of text with a single word in it' do
it 'copies as inline code' do
verify(
'.line[id="LC10"]',
'`end`'
'` end`'
)
end
end
@ -786,7 +786,7 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
it 'copies as inline code' do
verify(
'.line[id="LC9"]',
'`raise RuntimeError, "System commands must be given as an array of strings"`'
'` raise RuntimeError, "System commands must be given as an array of strings"`'
)
end
end
@ -825,7 +825,7 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
it 'copies as inline code' do
verify(
'.line[id="LC27"]',
'`"bio": null,`'
'` "bio": null,`'
)
end
end

View File

@ -15,7 +15,6 @@ import CommitRefs from '~/projects/commit_box/info/components/commit_refs.vue';
import {
mockCommitReferencesResponse,
mockOnlyBranchesResponse,
mockContainingBranchesResponse,
refsListPropsMock,
} from '../mock_data';
@ -54,7 +53,7 @@ describe('Commit references component', () => {
await createComponent();
});
it('renders component correcrly', () => {
it('renders component correctly', () => {
expect(findRefsLists()).toHaveLength(2);
});
@ -77,11 +76,6 @@ describe('Commit references component', () => {
expect(containingBranchesQueryHandler).toHaveBeenCalledTimes(1);
});
it('does not render list when there is no branches or tags', async () => {
await createComponent(successQueryHandler(mockOnlyBranchesResponse));
expect(findRefsLists()).toHaveLength(1);
});
describe('with relative url', () => {
beforeEach(async () => {
gon.relative_url_root = '/gitlab';

View File

@ -1,4 +1,4 @@
import { GlCollapse, GlButton, GlBadge, GlSkeletonLoader } from '@gitlab/ui';
import { GlCollapse, GlButton, GlBadge, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import RefsList from '~/projects/commit_box/info/components/refs_list.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
@ -23,11 +23,18 @@ describe('Commit references component', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findTippingRefs = () => wrapper.findAllComponents(GlBadge);
const findContainingRefs = () => wrapper.findComponent(GlCollapse);
const findEmptyMessage = () => wrapper.findByText('No related branches found');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
beforeEach(() => {
createComponent();
});
it('renders a loading icon when loading', () => {
createComponent({ isLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
});
it('renders the namespace passed', () => {
expect(findTitle().text()).toEqual(refsListPropsMock.namespace);
});
@ -65,11 +72,16 @@ describe('Commit references component', () => {
expect(refBadge.attributes('href')).toBe(refUrl);
});
it('does not reneder list of tipping branches or tags if there is no data', () => {
it('does not render list of tipping branches or tags if there is no data', () => {
createComponent({ tippingRefs: [] });
expect(findTippingRefs().exists()).toBe(false);
});
it('renders an empty message when there is no tipping and containing refs', () => {
createComponent({ tippingRefs: [] });
expect(findEmptyMessage().exists()).toBe(true);
});
it('renders skeleton loader when isLoading prop has true value', () => {
createComponent({ isLoading: true, containingRefs: [] });
expect(findSkeletonLoader().exists()).toBe(true);

View File

@ -3,6 +3,8 @@ import Bold from '~/content_editor/extensions/bold';
import Code from '~/content_editor/extensions/code';
import { createTestEditor } from '../test_utils';
const CODE_HTML = `<p dir="auto" data-sourcepos="1:1-1:31"><code data-sourcepos="1:2-1:30"> code with leading spaces</code></p>`;
describe('content_editor/extensions/code', () => {
let tiptapEditor;
let doc;
@ -31,6 +33,18 @@ describe('content_editor/extensions/code', () => {
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
describe('when parsing HTML', () => {
beforeEach(() => {
tiptapEditor.commands.setContent(CODE_HTML);
});
it('parses HTML correctly into an inline code block, preserving leading spaces', () => {
expect(tiptapEditor.getJSON()).toEqual(
doc(p(code(' code with leading spaces'))).toJSON(),
);
});
});
describe('shortcut: RightArrow', () => {
it('exits the code block', () => {
const initialDoc = doc(p('You can write ', code('java')));

View File

@ -9,12 +9,13 @@ import {
const { paragraph, code, italic, bold, strike } = builders;
it.each`
input | output
${'code'} | ${'`code`'}
${'code `with` backticks'} | ${'``code `with` backticks``'}
${'this is `inline-code`'} | ${'`` this is `inline-code` ``'}
${'`inline-code` in markdown'} | ${'`` `inline-code` in markdown ``'}
${'```js'} | ${'`` ```js ``'}
input | output
${'code'} | ${'`code`'}
${' code with leading spaces'} | ${'` code with leading spaces`'}
${'code `with` backticks'} | ${'``code `with` backticks``'}
${'this is `inline-code`'} | ${'`` this is `inline-code` ``'}
${'`inline-code` in markdown'} | ${'`` `inline-code` in markdown ``'}
${'```js'} | ${'`` ```js ``'}
`('correctly serializes inline code ("$input")', ({ input, output }) => {
expect(serialize(paragraph(code(input)))).toBe(output);
});

View File

@ -61,7 +61,7 @@ exports[`Merge request dashboard merge request component renders template 1`] =
>
<div
aria-label="5 comments, 100 file additions, 50 file deletions"
class="gl-flex gl-justify-end"
class="gl-flex gl-gap-3 gl-justify-end"
>
<div
class="gl-whitespace-nowrap"
@ -75,7 +75,7 @@ exports[`Merge request dashboard merge request component renders template 1`] =
5
</div>
<div
class="gl-ml-5 gl-whitespace-nowrap"
class="gl-whitespace-nowrap"
>
<gl-icon-stub
name="doc-code"
@ -87,7 +87,7 @@ exports[`Merge request dashboard merge request component renders template 1`] =
</span>
</div>
<div
class="gl-flex gl-font-bold gl-items-center gl-ml-3 gl-text-green-600"
class="gl-flex gl-font-bold gl-items-center gl-text-green-600"
>
<span>
+
@ -97,7 +97,7 @@ exports[`Merge request dashboard merge request component renders template 1`] =
</span>
</div>
<div
class="gl-flex gl-font-bold gl-items-center gl-ml-3 gl-text-red-500"
class="gl-flex gl-font-bold gl-items-center gl-text-red-500"
>
<span>

View File

@ -9,7 +9,7 @@ import {
describe('ExpirationToggle', () => {
let wrapper;
const value = 'foo';
const value = new Date().toISOString();
const findInput = () => wrapper.findComponent(GlFormInput);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
@ -44,7 +44,7 @@ describe('ExpirationToggle', () => {
describe('formattedValue', () => {
it.each`
valueProp | enabled | expected
${value} | ${true} | ${value}
${value} | ${true} | ${'July 6, 2020 at 12:00:00 AM GMT'}
${value} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
${undefined} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
${undefined} | ${true} | ${NOT_SCHEDULED_POLICY_TEXT}

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RemoveNamespaceFromOsTypeSbomComponents, feature_category: :software_composition_analysis do
RSpec.describe Gitlab::BackgroundMigration::RemoveNamespaceFromOsTypeSbomComponents, schema: 20240909204952, feature_category: :software_composition_analysis do
let(:components) { table(:sbom_components) }
let(:expected) do
(0...os_prefix_to_purl_type_mapping.size).map { |n| "package-#{n}" }

View File

@ -1302,4 +1302,46 @@ RSpec.describe Gitlab::Diff::File do
it { is_expected.to eq(false) }
end
end
describe '#line_side_code' do
let(:line) { instance_double(Gitlab::Diff::Line, type: 'old', old_pos: 4, new_pos: 4, added?: false, removed?: true, text: 'First Hunk Removed 1', meta?: false) }
it 'returns the correct left side ID' do
expect(diff_file.line_side_code(line, :old)).to eq("line_#{diff_file.file_hash}_L#{line.old_pos}")
end
it 'returns the correct right side ID' do
expect(diff_file.line_side_code(line, :new)).to eq("line_#{diff_file.file_hash}_R#{line.new_pos}")
end
end
describe '#text_diff' do
subject(:text_diff) { diff_file.text_diff? }
it 'returns true for text diffs' do
expect(text_diff).to eq(true)
end
it 'returns false for unchanged files' do
allow(diff_file).to receive(:modified_file?).and_return(false)
expect(text_diff).to eq(false)
end
it 'returns false for non text files' do
allow(diff_file).to receive(:text?).and_return(false)
expect(text_diff).to eq(false)
end
end
describe '#diff_lines_with_match_tail' do
subject(:lines) { diff_file.diff_lines_with_match_tail }
it { expect(lines.last.type).to eq('match') }
end
describe '#parallel_diff_lines_with_match_tail' do
subject(:lines) { diff_file.parallel_diff_lines_with_match_tail }
it { expect(lines.last[:left].type).to eq('match') }
end
end

View File

@ -60,5 +60,14 @@ RSpec.describe Gitlab::Diff::ParallelDiff do
expect(subject.parallelize).to eq(expected)
end
it 'can accept diff lines' do
diff_lines = [Gitlab::Diff::Line.new("", 'match', nil, 1, 1)]
expect(subject.parallelize(diff_lines)).to eq([
{
left: diff_lines[0], right: diff_lines[0]
}
])
end
end
end

View File

@ -0,0 +1,195 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Ci::CollectPipelineAnalyticsService, :click_house, :enable_admin_mode,
feature_category: :fleet_visibility do
include ClickHouseHelpers
let_it_be(:project1) { create(:project).tap(&:reload) } # reload required to calculate traversal path
let_it_be(:project2) { create(:project).tap(&:reload) }
let_it_be(:current_user) { create(:user, reporter_of: [project1, project2]) }
let_it_be(:starting_time) { Time.utc(2023) }
let_it_be(:ending_time) { 1.week.after(Time.utc(2023)) }
let(:project) { project1 }
let(:status_groups) { [:all] }
let(:from_time) { starting_time }
let(:to_time) { ending_time }
let(:service) do
described_class.new(
current_user: current_user,
project: project,
from_time: from_time,
to_time: to_time,
status_groups: status_groups)
end
let(:pipelines) do
[
create_pipeline(project1, :running, 35.minutes.before(ending_time), 30.minutes),
create_pipeline(project1, :success, 1.day.before(ending_time), 30.minutes),
create_pipeline(project1, :canceled, 1.hour.before(ending_time), 1.minute),
create_pipeline(project1, :failed, 5.days.before(ending_time), 2.hours),
create_pipeline(project1, :failed, 1.week.before(ending_time), 45.minutes),
create_pipeline(project1, :skipped, 5.days.before(ending_time), 1.second),
create_pipeline(project1, :skipped, 1.second.before(starting_time), 45.minutes),
create_pipeline(project1, :success, ending_time, 30.minutes)
]
end
subject(:result) { service.execute }
before do
insert_ci_pipelines_to_click_house(pipelines)
end
context 'when ClickHouse database is not configured' do
before do
allow(::Gitlab::ClickHouse).to receive(:configured?).and_return(false)
end
it 'returns error' do
expect(result.error?).to eq(true)
expect(result.errors).to contain_exactly('ClickHouse database is not configured')
end
end
shared_examples 'returns Not allowed error' do
it 'returns error' do
expect(result.error?).to eq(true)
expect(result.errors).to contain_exactly('Not allowed')
end
end
shared_examples 'a service returning aggregate analytics' do
using RSpec::Parameterized::TableSyntax
where(:status_groups, :expected_aggregate) do
%i[all] | { all: 6 }
%i[all success] | { all: 6, success: 1 }
%i[success other] | { success: 1, other: 2 }
%i[failed] | { failed: 2 }
end
with_them do
it 'returns aggregate analytics' do
expect(result.success?).to eq(true)
expect(result.errors).to eq([])
expect(result.payload[:aggregate]).to eq(expected_aggregate)
end
end
context 'when dates are not specified' do
let(:from_time) { nil }
let(:to_time) { nil }
context 'and there are pipelines in the last week', time_travel_to: '2023-01-08' do
it 'returns aggregate analytics from last week' do
expect(result.errors).to eq([])
expect(result.success?).to eq(true)
expect(result.payload[:aggregate]).to eq({ all: 6 })
end
end
context 'and there are no pipelines in the last week', time_travel_to: '2023-01-15 00:00:01' do
it 'returns empty aggregate analytics' do
expect(result.errors).to eq([])
expect(result.success?).to eq(true)
expect(result.payload[:aggregate]).to eq({ all: 0 })
end
end
end
context 'when requesting statistics starting one second before beginning of week' do
let(:from_time) { 1.second.before(starting_time) }
it 'does not include job starting 1 second before start of week' do
expect(result.errors).to eq([])
expect(result.success?).to eq(true)
expect(result.payload[:aggregate]).to eq({ all: 6 })
end
end
context 'when requesting statistics starting one hour before beginning of week' do
let(:from_time) { 1.hour.before(starting_time) }
it 'includes job starting 1 second before start of week' do
expect(result.errors).to eq([])
expect(result.success?).to eq(true)
expect(result.payload[:aggregate]).to eq({ all: 7 })
end
end
context 'when requesting hourly statistics that span more than one week' do
let(:from_time) { (1.hour + 1.second).before(starting_time) }
it 'returns an error' do
expect(result.errors).to contain_exactly(
"Maximum of #{described_class::TIME_BUCKETS_LIMIT} 1-hour intervals can be requested")
expect(result.error?).to eq(true)
end
end
context 'when a different project is specified' do
let(:project) { project2 }
let(:status_groups) { %i[all success failed] }
before do
insert_ci_pipelines_to_click_house([
create_pipeline(project2, :failed, 1.week.before(ending_time), 45.minutes)
])
end
it 'returns aggregate analytics for specified project only' do
expect(result.success?).to eq(true)
expect(result.errors).to eq([])
expect(result.payload[:aggregate]).to eq({ all: 1, success: 0, failed: 1 })
end
end
end
it_behaves_like 'a service returning aggregate analytics'
context 'when user is nil' do
let(:current_user) { nil }
include_examples 'returns Not allowed error'
end
context 'when project has analytics disabled' do
let_it_be(:project) { create(:project, :analytics_disabled) }
include_examples 'returns Not allowed error'
end
context 'when project is not specified' do
let(:project) { nil }
it 'returns error' do
expect(result.error?).to eq(true)
expect(result.errors).to contain_exactly('Project must be specified')
end
end
context 'when user is an admin' do
let(:current_user) { create(:admin) }
it_behaves_like 'a service returning aggregate analytics'
end
context 'when user is a guest' do
let_it_be(:current_user) { create(:user, guest_of: project1) }
include_examples 'returns Not allowed error'
end
def create_pipeline(project, status, started_at, duration)
build_stubbed(:ci_pipeline, status,
project: project,
created_at: 1.second.before(started_at), started_at: started_at, finished_at: duration.after(started_at),
duration: duration)
end
end

View File

@ -41,6 +41,18 @@ module ClickHouseHelpers
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
def insert_ci_pipelines_to_click_house(pipelines)
result = clickhouse_fixture(:ci_finished_pipelines, pipelines.map do |pipeline|
pipeline.slice(
%i[id duration status source ref committed_at created_at started_at finished_at]).symbolize_keys
.merge(
path: pipeline.project&.project_namespace&.traversal_path || '0/'
)
end)
expect(result).to eq(true)
end
def self.default_timezone
ActiveRecord.default_timezone
end

View File

@ -1876,7 +1876,6 @@
- './ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb'
- './ee/spec/requests/api/internal/base_spec.rb'
- './ee/spec/requests/api/internal/kubernetes_spec.rb'
- './ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb'
- './ee/spec/requests/api/invitations_spec.rb'
- './ee/spec/requests/api/issue_links_spec.rb'
- './ee/spec/requests/api/issues_spec.rb'
@ -2462,7 +2461,6 @@
- './ee/spec/services/todos/allowed_target_filter_service_spec.rb'
- './ee/spec/services/todos/destroy/confidential_epic_service_spec.rb'
- './ee/spec/services/todo_service_spec.rb'
- './ee/spec/services/upcoming_reconciliations/update_service_spec.rb'
- './ee/spec/services/user_permissions/export_service_spec.rb'
- './ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb'
- './ee/spec/services/users/abuse/namespace_bans/create_service_spec.rb'

View File

@ -22,6 +22,51 @@ RSpec.shared_examples_for 'a ci_finished_pipelines aggregation model' do |table_
end
end
describe '#within_dates' do
let(:from_time) { 1.hour.ago }
let(:to_time) { Time.current }
subject(:result_sql) { instance.within_dates(from_time, to_time).to_sql }
it 'builds the correct SQL' do
expected_sql = <<~SQL.lines(chomp: true).join(' ')
SELECT * FROM "#{table_name}"
WHERE "#{table_name}"."started_at_bucket" >= toDateTime64('#{from_time.utc.strftime('%Y-%m-%d %H:%M:%S')}', 6, 'UTC')
AND "#{table_name}"."started_at_bucket" < toDateTime64('#{to_time.utc.strftime('%Y-%m-%d %H:%M:%S')}', 6, 'UTC')
SQL
expect(result_sql.strip).to eq(expected_sql.strip)
end
context 'when only from_date is passed' do
let(:from_time) { 1.hour.ago }
let(:to_time) { nil }
it 'builds the correct SQL' do
expected_sql = <<~SQL.lines(chomp: true).join(' ')
SELECT * FROM "#{table_name}"
WHERE "#{table_name}"."started_at_bucket" >= toDateTime64('#{from_time.utc.strftime('%Y-%m-%d %H:%M:%S')}', 6, 'UTC')
SQL
expect(result_sql.strip).to eq(expected_sql.strip)
end
end
context 'when only to_date is passed' do
let(:from_time) { nil }
let(:to_time) { Time.current }
it 'builds the correct SQL' do
expected_sql = <<~SQL.lines(chomp: true).join(' ')
SELECT * FROM "#{table_name}"
WHERE "#{table_name}"."started_at_bucket" < toDateTime64('#{to_time.utc.strftime('%Y-%m-%d %H:%M:%S')}', 6, 'UTC')
SQL
expect(result_sql.strip).to eq(expected_sql.strip)
end
end
end
describe '#by_status' do
subject(:result_sql) { instance.by_status(%i[failed success]).to_sql }
@ -98,13 +143,25 @@ RSpec.shared_examples_for 'a ci_finished_pipelines aggregation model' do |table_
end
it 'builds the correct SQL with chained methods' do
from_time = 1.hour.ago
to_time = Time.current
expected_sql = <<~SQL.lines(chomp: true).join(' ')
SELECT "#{table_name}"."status" FROM "#{table_name}"
WHERE "#{table_name}"."path" = '#{path}'
AND "#{table_name}"."started_at_bucket" >= toDateTime64('#{from_time.utc.strftime('%Y-%m-%d %H:%M:%S')}', 6, 'UTC')
AND "#{table_name}"."started_at_bucket" < toDateTime64('#{to_time.utc.strftime('%Y-%m-%d %H:%M:%S')}', 6, 'UTC')
AND "#{table_name}"."status" IN ('failed', 'success')
GROUP BY "#{table_name}"."status"
SQL
result_sql = instance
.for_project(project)
.select(:status)
.within_dates(from_time, to_time)
.by_status(%i[failed success])
.group_by_status.to_sql
expect(result_sql.strip).to eq(expected_sql.strip)
end
end

View File

@ -724,9 +724,11 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :reestablished_active_recor
end
def expect_logger_to_receive_messages(messages)
expect_any_instance_of(Gitlab::BackupLogger) do |logger|
messages.each do |message|
allow(logger).to receive(:info).with(message).ordered
[Gitlab::BackupLogger, Gitlab::Backup::JsonLogger].each do |log_class|
expect_any_instance_of(log_class) do |logger|
messages.each do |message|
allow(logger).to receive(:info).with(message).ordered
end
end
end
end

View File

@ -122,6 +122,26 @@ RSpec.describe RemoveExpiredMembersWorker, feature_category: :system_access do
'meta.user' => expired_group_member.user.username
)
end
context 'when the user has a direct membership in a subproject' do
let_it_be(:subproject) { create(:project, group: expired_group_member.group) }
let_it_be(:non_expired_project_membership) { create(:project_member, user: expired_group_member.user, access_level: ProjectMember::MAINTAINER, project: subproject) }
it 'does not expire the membership in the subgroup' do
worker.perform
expect(non_expired_project_membership.reload).to be_present
end
end
context 'when the user has a direct membership in a subgroup' do
let_it_be(:subgroup) { create(:group, parent: expired_group_member.group) }
let_it_be(:non_expired_group_membership) { create(:group_member, user: expired_group_member.user, access_level: GroupMember::MAINTAINER, group: subgroup) }
it 'does not expire the membership in the subgroup' do
worker.perform
expect(non_expired_group_membership.reload).to be_present
end
end
end
context 'when the last group owner expires' do

View File

@ -24,12 +24,7 @@ cmd/gitlab-zip-metadata/limit/reader.go:37:1: exported: exported function NewLim
cmd/gitlab-zip-metadata/main.go:1:1: package-comments: should have a package comment (revive)
cmd/gitlab-zip-metadata/main.go:17:5: exported: exported var Version should have comment or be unexported (revive)
cmd/gitlab-zip-metadata/main.go:66:9: superfluous-else: if block ends with call to os.Exit function, so drop this else and outdent its block (revive)
internal/api/api.go:146:2: var-naming: don't use ALL_CAPS in Go names; use CamelCase (revive)
internal/api/api.go:149:2: var-naming: don't use ALL_CAPS in Go names; use CamelCase (revive)
internal/api/api.go:153:2: var-naming: don't use ALL_CAPS in Go names; use CamelCase (revive)
internal/api/block_test.go:61:34: response body must be closed (bodyclose)
internal/api/channel_settings.go:57:28: G402: TLS MinVersion too low. (gosec)
internal/builds/register.go:120: Function 'RegisterHandler' is too long (66 > 60) (funlen)
internal/channel/channel.go:128:31: response body must be closed (bodyclose)
internal/config/config.go:1:1: package-comments: should have a package comment (revive)
internal/config/config.go:27:7: exported: exported const Megabyte should have comment or be unexported (revive)

View File

@ -143,14 +143,14 @@ type RemoteObject struct {
type Response struct {
// GL_ID is an environment variable used by gitlab-shell hooks during 'git
// push' and 'git pull'
GL_ID string
GL_ID string // nolint:stylecheck,revive // used as env variable
// GL_USERNAME holds gitlab username of the user who is taking the action causing hooks to be invoked
GL_USERNAME string
GL_USERNAME string // nolint:stylecheck,revive // used as env variable
// GL_REPOSITORY is an environment variable used by gitlab-shell hooks during
// 'git push' and 'git pull'
GL_REPOSITORY string
GL_REPOSITORY string // nolint:stylecheck,revive // used as env variable
// GitConfigOptions holds the custom options that we want to pass to the git command
GitConfigOptions []string

View File

@ -58,7 +58,7 @@ func TestBlocker(t *testing.T) {
func TestBlockerFlushable(t *testing.T) {
rw := httptest.NewRecorder()
b := blocker{rw: rw}
rc := http.NewResponseController(&b)
rc := http.NewResponseController(&b) //nolint:bodyclose
err := rc.Flush()
require.NoError(t, err, "the underlying response writer is not flushable")

View File

@ -117,6 +117,7 @@ func watchForRunnerChange(ctx context.Context, watchHandler WatchKeyHandler, tok
return watchHandler(ctx, runnerBuildQueue+token, lastUpdate, duration)
}
// RegisterHandler with key watch logic if polling is enabled.
func RegisterHandler(h http.Handler, watchHandler WatchKeyHandler, pollingDuration time.Duration) http.Handler {
if pollingDuration == 0 {
return h
@ -135,16 +136,8 @@ func RegisterHandler(h http.Handler, watchHandler WatchKeyHandler, pollingDurati
newRequest := cloneRequestWithNewBody(r, requestBody)
runnerRequest, err := readRunnerRequest(r, requestBody)
if err != nil {
registerHandlerBodyParseErrors.Inc()
proxyRegisterRequest(h, w, newRequest)
return
}
if runnerRequest.Token == "" || runnerRequest.LastUpdate == "" {
registerHandlerMissingValues.Inc()
proxyRegisterRequest(h, w, newRequest)
runnerRequest, shouldReturn := getRunnerRequest(r, requestBody, h, w, newRequest)
if shouldReturn {
return
}
@ -186,6 +179,22 @@ func RegisterHandler(h http.Handler, watchHandler WatchKeyHandler, pollingDurati
})
}
func getRunnerRequest(r *http.Request, requestBody []byte, h http.Handler, w http.ResponseWriter, newRequest *http.Request) (*runnerRequest, bool) {
runnerRequest, err := readRunnerRequest(r, requestBody)
if err != nil {
registerHandlerBodyParseErrors.Inc()
proxyRegisterRequest(h, w, newRequest)
return nil, true
}
if runnerRequest.Token == "" || runnerRequest.LastUpdate == "" {
registerHandlerMissingValues.Inc()
proxyRegisterRequest(h, w, newRequest)
return nil, true
}
return runnerRequest, false
}
func cloneRequestWithNewBody(r *http.Request, body []byte) *http.Request {
newReq := r.Clone(r.Context())
newReq.Body = io.NopCloser(bytes.NewReader(body))