Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-16 15:07:53 +00:00
parent 12166c0faf
commit c1f98d9590
144 changed files with 1218 additions and 431 deletions

View File

@ -104,7 +104,8 @@ yarn-audit-dependency_scanning:
- mkdir ~/.aws
- '[[ -z "${AWS_SIEM_REPORT_INGESTION_CREDENTIALS_FILE}" ]] || mv "${AWS_SIEM_REPORT_INGESTION_CREDENTIALS_FILE}" ~/.aws/credentials'
- npm install --no-save --ignore-scripts @aws-sdk/client-s3@3.49.0
- scripts/ingest-reports-to-siem
- scripts/ingest-reports-to-siem || true # Allow legacy report to fail as we'll remove it in the future anyway
- scripts/ingest-reports-to-siem-devo
artifacts:
paths:
- gl-dependency-scanning-report.json

View File

@ -1 +1 @@
f223d8cbcb6319356cb9f746252b15e541695d2f
7910fc0e132402d3ab22cef73924aef360598f35

View File

@ -348,7 +348,7 @@ gem 'pg_query', '~> 2.2', '>= 2.2.1'
gem 'premailer-rails', '~> 1.10.3'
gem 'gitlab-labkit', '~> 0.30.1'
gem 'gitlab-labkit', '~> 0.31.0'
gem 'thrift', '>= 0.16.0'
# I18n
@ -373,7 +373,7 @@ gem 'prometheus-client-mmap', '~> 0.17', require: 'prometheus/client'
gem 'warning', '~> 1.3.0'
group :development do
gem 'lefthook', '~> 1.2.8', require: false
gem 'lefthook', '~> 1.2.9', require: false
gem 'rubocop'
gem 'solargraph', '~> 0.47.2', require: false

View File

@ -203,7 +203,7 @@
{"name":"gitlab-dangerfiles","version":"3.7.0","platform":"ruby","checksum":"35c5bc42e60c575ab5701192ca2384ab414b14c2963602b39e143b1aaeb7e54d"},
{"name":"gitlab-experiment","version":"0.7.1","platform":"ruby","checksum":"166dddb3aa83428bcaa93c35684ed01dc4d61f321fd2ae40b020806dc54a7824"},
{"name":"gitlab-fog-azure-rm","version":"1.4.0","platform":"ruby","checksum":"af4163c32b028aa5208814a3f4765a5817d50527e6c61931f766bf18a2e0eb7e"},
{"name":"gitlab-labkit","version":"0.30.1","platform":"ruby","checksum":"bdedbd86014c83dfd6a50d20dbc1709697bba2bb9e3666383e5f28cbd312b113"},
{"name":"gitlab-labkit","version":"0.31.0","platform":"ruby","checksum":"5b044c4ededd7005e6d1ca5a53ac5f9d7a4d12a7363673fbc898e1844246ed1f"},
{"name":"gitlab-license","version":"2.2.1","platform":"ruby","checksum":"39fcf6be8b2887df8afe01b5dcbae8d08b7c5d937ff56b0fb40484a8c4f02d30"},
{"name":"gitlab-mail_room","version":"0.0.9","platform":"ruby","checksum":"6700374b5c0aa9d9ad4e711aeb677f0b7d415a6d01d3baa699efab25349d851c"},
{"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"},
@ -314,7 +314,7 @@
{"name":"kramdown","version":"2.3.2","platform":"ruby","checksum":"cb4530c2e9d16481591df2c9336723683c354e5416a5dd3e447fa48215a6a71c"},
{"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"},
{"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"},
{"name":"lefthook","version":"1.2.8","platform":"ruby","checksum":"3776de22e0a3de8fc8c4c58d8b42cd5eeb1cd291232a09a8dc5335f0ad505a7c"},
{"name":"lefthook","version":"1.2.9","platform":"ruby","checksum":"1fd4a768e08fc624e756597fc628b3c7991267325974a7a5cc169595b425701d"},
{"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"},
{"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"},
{"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"},

View File

@ -584,7 +584,7 @@ GEM
fog-json (~> 1.2.0)
mime-types
ms_rest_azure (~> 0.12.0)
gitlab-labkit (0.30.1)
gitlab-labkit (0.31.0)
actionpack (>= 5.0.0, < 8.0.0)
activesupport (>= 5.0.0, < 8.0.0)
grpc (>= 1.37)
@ -845,7 +845,7 @@ GEM
kramdown (~> 2.0)
launchy (2.5.0)
addressable (~> 2.7)
lefthook (1.2.8)
lefthook (1.2.9)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (2.0.0)
@ -1679,7 +1679,7 @@ DEPENDENCIES
gitlab-dangerfiles (~> 3.7.0)
gitlab-experiment (~> 0.7.1)
gitlab-fog-azure-rm (~> 1.4.0)
gitlab-labkit (~> 0.30.1)
gitlab-labkit (~> 0.31.0)
gitlab-license (~> 2.2.1)
gitlab-mail_room (~> 0.0.9)
gitlab-markup (~> 1.9.0)
@ -1738,7 +1738,7 @@ DEPENDENCIES
knapsack (~> 1.21.1)
kramdown (~> 2.3.1)
kubeclient (~> 4.9.3)!
lefthook (~> 1.2.8)
lefthook (~> 1.2.9)
letter_opener_web (~> 2.0.0)
license_finder (~> 7.0)
licensee (~> 9.15)

View File

@ -65,7 +65,9 @@ const STATUS_MAP = {
};
function isIncompleteImport(stats) {
return Object.keys(stats.fetched).some((key) => stats.fetched[key] !== stats.imported[key]);
return Object.keys(stats?.fetched ?? []).some(
(key) => stats.fetched[key] !== stats.imported[key],
);
}
export default {
@ -91,7 +93,9 @@ export default {
computed: {
knownStats() {
const knownStatisticKeys = Object.keys(STATISTIC_ITEMS);
return Object.keys(this.stats.fetched).filter((key) => knownStatisticKeys.includes(key));
return Object.keys(this.stats?.fetched ?? []).filter((key) =>
knownStatisticKeys.includes(key),
);
},
hasStats() {

View File

@ -182,16 +182,16 @@ export default {
<div v-if="repositories.length" class="gl-w-full">
<table>
<thead class="gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100">
<th class="import-jobs-from-col gl-p-4 gl-vertical-align-top gl-border-b-1">
<th class="gl-w-half gl-p-4 gl-vertical-align-top gl-border-b-1">
{{ fromHeaderText }}
</th>
<th class="import-jobs-to-col gl-p-4 gl-vertical-align-top gl-border-b-1">
<th class="gl-w-half gl-p-4 gl-vertical-align-top gl-border-b-1">
{{ __('To GitLab') }}
</th>
<th class="import-jobs-status-col gl-p-4 gl-vertical-align-top gl-border-b-1">
<th class="gl-p-4 gl-vertical-align-top gl-border-b-1">
{{ __('Status') }}
</th>
<th class="import-jobs-cta-col gl-p-4 gl-vertical-align-top gl-border-b-1"></th>
<th class="gl-p-4 gl-vertical-align-top gl-border-b-1"></th>
</thead>
<tbody>
<template v-for="repo in repositories">

View File

@ -9,6 +9,8 @@ import {
GlDropdownDivider,
GlDropdownSectionHeader,
GlTooltip,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
@ -32,6 +34,10 @@ export default {
GlBadge,
GlLink,
GlTooltip,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
repo: {
@ -53,6 +59,12 @@ export default {
},
},
data() {
return {
isSelectedForReimport: false,
};
},
computed: {
...mapState(['ciCdOnly']),
...mapGetters(['getImportTarget']),
@ -94,7 +106,11 @@ export default {
},
importButtonText() {
return this.ciCdOnly ? __('Connect') : __('Import');
if (this.ciCdOnly) {
return __('Connect');
}
return this.isFinished ? __('Re-import') : __('Import');
},
newNameInput: {
@ -115,6 +131,22 @@ export default {
importTarget: { ...this.importTarget, ...changedValues },
});
},
handleImportRepo() {
if (this.isFinished && !this.isSelectedForReimport) {
this.isSelectedForReimport = true;
this.$nextTick(() => {
this.$refs.newNameInput.$el.focus();
});
} else {
this.isSelectedForReimport = false;
this.fetchImport({
repoId: this.repo.importSource.id,
optionalStages: this.optionalStages,
});
}
},
},
helpUrl: helpPagePath('/user/project/import/github.md'),
@ -132,6 +164,20 @@ export default {
>{{ repo.importSource.fullName }}
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</gl-link>
<div v-if="isFinished" class="gl-font-sm">
<gl-sprintf :message="s__('BulkImport|Last imported to %{link}')">
<template #link>
<gl-link
:href="repo.importedProject.fullPath"
class="gl-font-sm"
target="_blank"
data-qa-selector="go_to_project_link"
>
{{ displayFullPath }}
</gl-link>
</template>
</gl-sprintf>
</div>
</td>
<td
class="gl-display-flex gl-sm-flex-wrap gl-p-4 gl-pt-5 gl-vertical-align-top"
@ -139,7 +185,7 @@ export default {
data-qa-selector="project_path_content"
>
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted">
<template v-else-if="isImportNotStarted || isSelectedForReimport">
<div class="gl-display-flex gl-align-items-stretch gl-w-full">
<import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace">
<template v-if="namespaces.length">
@ -166,6 +212,7 @@ export default {
/
</div>
<gl-form-input
ref="newNameInput"
v-model="newNameInput"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
data-qa-selector="project_path_field"
@ -177,7 +224,7 @@ export default {
<td class="gl-p-4 gl-vertical-align-top" data-qa-selector="import_status_indicator">
<import-status :status="importStatus" :stats="stats" />
</td>
<td data-testid="actions" class="gl-vertical-align-top gl-pt-4">
<td data-testid="actions" class="gl-vertical-align-top gl-pt-4 gl-white-space-nowrap">
<gl-tooltip :target="() => $refs.cancelButton.$el">
<div class="gl-text-left">
<p class="gl-mb-5 gl-font-weight-bold">{{ s__('ImportProjects|Cancel import') }}</p>
@ -199,22 +246,26 @@ export default {
@click="cancelImport({ repoId: repo.importSource.id })"
/>
<gl-button
v-if="isFinished"
class="btn btn-default"
:href="repo.importedProject.fullPath"
rel="noreferrer noopener"
target="_blank"
data-qa-selector="go_to_project_button"
>{{ __('Go to project') }}
</gl-button>
<gl-button
v-if="isImportNotStarted"
v-if="isImportNotStarted || isFinished"
type="button"
data-qa-selector="import_button"
@click="fetchImport({ repoId: repo.importSource.id, optionalStages })"
@click="handleImportRepo()"
>
{{ importButtonText }}
</gl-button>
<gl-icon
v-if="isFinished"
v-gl-tooltip
:size="16"
name="information-o"
:title="
s__(
'ImportProjects|Re-import creates a new project. It does not sync with the existing project.',
)
"
class="gl-ml-3"
/>
<gl-badge v-else-if="isIncompatible" variant="danger">{{
__('Incompatible project')
}}</gl-badge>

View File

@ -2,16 +2,6 @@ import Vue from 'vue';
import { STATUSES } from '../../constants';
import * as types from './mutation_types';
const makeNewImportedProject = (importedProject) => ({
importSource: {
id: importedProject.id,
fullName: importedProject.importSource,
sanitizedName: importedProject.name,
providerLink: importedProject.providerLink,
},
importedProject: { ...importedProject },
});
const makeNewIncompatibleProject = (project) => ({
importSource: { ...project, incompatible: true },
importedProject: null,
@ -55,14 +45,6 @@ export default {
// Legacy code path, will be removed when all importers will be switched to new pagination format
// https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
const newImportedProjects = processLegacyEntries({
newRepositories: repositories.importedProjects.filter(
(p) => p.importStatus !== STATUSES.CANCELED,
),
existingRepositories: state.repositories,
factory: makeNewImportedProject,
});
const incompatibleRepos = repositories.incompatibleRepos ?? [];
const newIncompatibleProjects = processLegacyEntries({
newRepositories: incompatibleRepos,
@ -70,16 +52,22 @@ export default {
factory: makeNewIncompatibleProject,
});
const existingProjects = [...newImportedProjects, ...state.repositories];
const existingProjectNames = new Set(existingProjects.map((p) => p.importSource.fullName));
const existingProjectNames = new Set(state.repositories.map((p) => p.importSource.fullName));
const importedProjects = [...(repositories.importedProjects ?? [])].reverse();
const newProjects = repositories.providerRepos
.filter((project) => !existingProjectNames.has(project.fullName))
.map((project) => ({
importSource: project,
importedProject: null,
}));
.map((project) => {
const importedProject = importedProjects.find(
(p) => p.providerLink === project.providerLink,
);
state.repositories = [...existingProjects, ...newProjects, ...newIncompatibleProjects];
return {
importSource: project,
importedProject,
};
});
state.repositories = [...state.repositories, ...newProjects, ...newIncompatibleProjects];
if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
state.pageInfo.page -= 1;
@ -113,7 +101,7 @@ export default {
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
const existingRepo = state.repositories.find((r) => r.importSource.id === repoId);
existingRepo.importedProject = null;
existingRepo.importedProject.importStatus = STATUSES.FAILED;
},
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {

View File

@ -11,7 +11,7 @@ export function getImportStatus(project) {
export function isProjectImportable(project) {
return (
!isIncompatible(project) &&
[STATUSES.NONE, STATUSES.CANCELED].includes(getImportStatus(project))
[STATUSES.NONE, STATUSES.CANCELED, STATUSES.FAILED].includes(getImportStatus(project))
);
}

View File

@ -86,10 +86,8 @@ export default {
</span>
<template v-else>
<span class="gl-ml-0! gl-text-body! gl-flex-grow-1 gl-w-full gl-md-w-auto gl-mr-2">
<bold-text :message="$options.i18n.shouldBeResolved" />
<span v-if="!userPermissions.canMerge">
{{ $options.i18n.usersWriteBranches }}
</span>
<bold-text v-if="userPermissions.canMerge" :message="$options.i18n.shouldBeResolved" />
<bold-text v-else :message="$options.i18n.usersWriteBranches" />
</span>
</template>
</template>

View File

@ -277,11 +277,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
flex-flow: row wrap;
}
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2098
.gl-max-w-0 {
max-width: 0;
}
.gl-isolate {
isolation: isolate;
}

View File

@ -205,7 +205,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def reorder
service = ::Issues::ReorderService.new(project: project, current_user: current_user, params: reorder_params)
service = ::Issues::ReorderService.new(container: project, current_user: current_user, params: reorder_params)
if service.execute(issue)
head :ok
@ -216,7 +216,7 @@ class Projects::IssuesController < Projects::ApplicationController
def related_branches
@related_branches = ::Issues::RelatedBranchesService
.new(project: project, current_user: current_user)
.new(container: project, current_user: current_user)
.execute(issue)
.map { |branch| branch.merge(link: branch_link(branch)) }
@ -371,7 +371,7 @@ class Projects::IssuesController < Projects::ApplicationController
def update_service
spam_params = ::Spam::SpamParams.new_from_request(request: request)
::Issues::UpdateService.new(project: project, current_user: current_user, params: issue_params, spam_params: spam_params)
::Issues::UpdateService.new(container: project, current_user: current_user, params: issue_params, spam_params: spam_params)
end
def finder_type

View File

@ -9,20 +9,39 @@ module Ci
@pipeline_schedules = project.pipeline_schedules
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(scope: nil)
scoped_schedules =
case scope
when 'active'
pipeline_schedules.active
when 'inactive'
pipeline_schedules.inactive
else
pipeline_schedules
end
def execute(scope: nil, ids: nil)
items = pipeline_schedules
items = by_ids(items, ids)
items = by_scope(items, scope)
scoped_schedules.order(id: :desc)
sort_items(items)
end
# rubocop: enable CodeReuse/ActiveRecord
private
def by_ids(items, ids)
if ids.present?
items.id_in(ids)
else
items
end
end
def by_scope(items, scope)
case scope
when 'active'
items.active
when 'inactive'
items.inactive
else
items
end
end
# rubocop:disable CodeReuse/ActiveRecord
def sort_items(items)
items.order(id: :desc)
end
# rubocop:enable CodeReuse/ActiveRecord
end
end

View File

@ -33,7 +33,7 @@ module Mutations
def assign!(resource, users, operation_mode)
update_service_class.new(
project: resource.project,
**update_service_class.constructor_container_arg(resource.project),
current_user: current_user,
params: { assignee_ids: assignee_ids(resource, users, operation_mode) }
).execute(resource)

View File

@ -19,7 +19,7 @@ module Mutations
# spam_params so a check can be performed.
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential }, spam_params: spam_params)
::Issues::UpdateService.new(container: project, current_user: current_user, params: { confidential: confidential }, spam_params: spam_params)
.execute(issue)
check_spam_action_response!(issue)

View File

@ -14,7 +14,7 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project: project, current_user: current_user, params: { due_date: due_date })
::Issues::UpdateService.new(container: project, current_user: current_user, params: { due_date: due_date })
.execute(issue)
{

View File

@ -17,7 +17,7 @@ module Mutations
check_feature_availability!(issue)
::Issues::UpdateService.new(
project: project,
container: project,
current_user: current_user,
params: { escalation_status: { status: status } }
).execute(issue)

View File

@ -13,7 +13,7 @@ module Mutations
def resolve(project_path:, iid:, locked:)
issue = authorized_find!(project_path: project_path, iid: iid)
::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: { discussion_locked: locked })
::Issues::UpdateService.new(container: issue.project, current_user: current_user, params: { discussion_locked: locked })
.execute(issue)
{

View File

@ -15,7 +15,7 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project: project, current_user: current_user, params: { severity: severity })
::Issues::UpdateService.new(container: project, current_user: current_user, params: { severity: severity })
.execute(issue)
{

View File

@ -42,7 +42,7 @@ module Mutations
args = parse_arguments(args)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue)
::Issues::UpdateService.new(container: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue)
{
issue: issue,

View File

@ -25,7 +25,7 @@ module Mutations
interpret_quick_actions!(work_item, current_user, widget_params, attributes)
update_result = ::WorkItems::UpdateService.new(
project: work_item.project,
container: work_item.project,
current_user: current_user,
params: attributes,
widget_params: widget_params,

View File

@ -32,7 +32,7 @@ module Mutations
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::WorkItems::UpdateService.new(
project: task.project,
container: task.project,
current_user: current_user,
params: task_data_hash.except(:id),
spam_params: spam_params

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module Resolvers
class DataTransferResolver < BaseResolver
argument :from, Types::DateType,
description: 'Retain egress data for 1 year. Current month will increase dynamically as egress occurs.',
required: false
argument :to, Types::DateType,
description: 'End date for the data.',
required: false
type ::Types::DataTransfer::BaseType, null: false
def self.source
raise NotImplementedError
end
def self.project
Class.new(self) do
type Types::DataTransfer::ProjectDataTransferType, null: false
def self.source
"Project"
end
end
end
def self.group
Class.new(self) do
type Types::DataTransfer::GroupDataTransferType, null: false
def self.source
"Group"
end
end
end
def resolve(**_args)
return unless Feature.enabled?(:data_transfer_monitoring)
start_date = Date.new(2023, 0o1, 0o1)
date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') }
nodes = 0.upto(3).map do |i|
{
date: date_for_index.call(i),
repository_egress: 250_000,
artifacts_egress: 250_000,
packages_egress: 250_000,
registry_egress: 250_000
}
end
{ egress_nodes: nodes }
end
end
end

View File

@ -10,8 +10,13 @@ module Resolvers
required: false,
description: 'Filter pipeline schedules by active status.'
def resolve(status: nil)
::Ci::PipelineSchedulesFinder.new(project).execute(scope: status)
argument :ids, [GraphQL::Types::ID],
required: false,
default_value: nil,
description: 'Filter pipeline schedules by IDs.'
def resolve(status: nil, ids: nil)
::Ci::PipelineSchedulesFinder.new(project).execute(scope: status, ids: ids)
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Types
module DataTransfer
class BaseType < BaseObject
authorize
field :egress_nodes, type: Types::DataTransfer::EgressNodeType.connection_type,
description: 'Data nodes.',
null: true # disallow null once data_transfer_monitoring feature flag is rolled-out!
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Types
module DataTransfer
class EgressNodeType < BaseObject
authorize
field :date, GraphQL::Types::String,
description: 'First day of the node range. There is one node per month.',
null: false
field :total_egress, GraphQL::Types::BigInt,
description: 'Total egress for that project in that period of time.',
null: false
field :repository_egress, GraphQL::Types::BigInt,
description: 'Repository egress for that project in that period of time.',
null: false
field :artifacts_egress, GraphQL::Types::BigInt,
description: 'Artifacts egress for that project in that period of time.',
null: false
field :packages_egress, GraphQL::Types::BigInt,
description: 'Packages egress for that project in that period of time.',
null: false
field :registry_egress, GraphQL::Types::BigInt,
description: 'Registery egress for that project in that period of time.',
null: false
def total_egress
object.values.select { |x| x.is_a?(Integer) }.sum
end
end
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Types
module DataTransfer
class GroupDataTransferType < BaseType
graphql_name 'GroupDataTransfer'
authorize
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Types
module DataTransfer
class ProjectDataTransferType < BaseType
graphql_name 'ProjectDataTransfer'
authorize
field :total_egress, GraphQL::Types::BigInt,
description: 'Total egress for that project in that period of time.',
null: true # disallow null once data_transfer_monitoring feature flag is rolled-out!
def total_egress(**_)
return unless Feature.enabled?(:data_transfer_monitoring)
40_000_000
end
end
end
end

View File

@ -239,6 +239,11 @@ module Types
description: 'Releases belonging to projects in the group.',
resolver: Resolvers::GroupReleasesResolver
field :data_transfer, Types::DataTransfer::GroupDataTransferType,
null: true,
resolver: Resolvers::DataTransferResolver.group,
description: 'Data transfer data point for a specific period. This is mocked data under a development feature flag.'
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: group) do |titles, loader, args|
LabelsFinder

View File

@ -190,7 +190,7 @@ module Types
def related_merge_requests
# rubocop: disable CodeReuse/ActiveRecord
MergeRequest.where(
id: ::Issues::ReferencedMergeRequestsService.new(project: object.project, current_user: current_user)
id: ::Issues::ReferencedMergeRequestsService.new(container: object.project, current_user: current_user)
.execute(object)
.first
.map(&:id)

View File

@ -566,6 +566,11 @@ module Types
resolver: ::Resolvers::Ci::ProjectRunnersResolver,
description: "Find runners visible to the current user."
field :data_transfer, Types::DataTransfer::ProjectDataTransferType,
null: true, # disallow null once data_transfer_monitoring feature flag is rolled-out!
resolver: Resolvers::DataTransferResolver.project,
description: 'Data transfer data point for a specific period. This is mocked data under a development feature flag.'
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
end

View File

@ -167,6 +167,10 @@ module ApplicationSettingsHelper
" using their classification label.")
end
def external_authorization_allow_token_help_text
s_("ExternalAuthorization|Does not apply if service URL is specified.")
end
def external_authorization_timeout_help_text
s_("ExternalAuthorization|Period GitLab waits for a response from the external "\
"service. If there is no response, access is denied. Default: 0.5 seconds.")
@ -499,7 +503,8 @@ module ApplicationSettingsHelper
:external_authorization_service_default_label,
:external_authorization_service_enabled,
:external_authorization_service_timeout,
:external_authorization_service_url
:external_authorization_service_url,
:allow_deploy_tokens_and_keys_with_external_authn
]
end

View File

@ -3,9 +3,11 @@
module Ci
class GroupVariable < Ci::ApplicationRecord
include Ci::HasVariable
include Presentable
include Ci::Maskable
include Ci::RawVariable
include Limitable
include Presentable
prepend HasEnvironmentScope
belongs_to :group, class_name: "::Group"
@ -21,6 +23,9 @@ module Ci
scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) }
scope :for_groups, ->(group_ids) { where(group_id: group_ids) }
self.limit_name = 'group_ci_variables'
self.limit_scope = :group
def audit_details
key
end

View File

@ -134,9 +134,6 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
# We will start using this column once we complete https://gitlab.com/gitlab-org/gitlab/-/issues/285597
ignore_column :original_filename, remove_with: '14.7', remove_after: '2022-11-22'
mount_file_store_uploader JobArtifactUploader, skip_store_file: true
before_save :set_size, if: :file_changed?

View File

@ -3,9 +3,11 @@
module Ci
class Variable < Ci::ApplicationRecord
include Ci::HasVariable
include Presentable
include Ci::Maskable
include Ci::RawVariable
include Limitable
include Presentable
prepend HasEnvironmentScope
belongs_to :project
@ -20,6 +22,9 @@ module Ci
scope :unprotected, -> { where(protected: false) }
scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) }
self.limit_name = 'project_ci_variables'
self.limit_scope = :project
def audit_details
key
end

View File

@ -52,7 +52,7 @@ module Boards
end
def update(issue, issue_modification_params)
::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: issue_modification_params).execute(issue)
::Issues::UpdateService.new(container: issue.project, current_user: current_user, params: issue_modification_params).execute(issue)
end
def moving_to_list_items_relation

View File

@ -37,7 +37,7 @@ module ErrorTracking
def close_issue(issue)
Issues::CloseService
.new(project: project, current_user: current_user)
.new(container: project, current_user: current_user)
.execute(issue, system_note: false)
end

View File

@ -82,7 +82,7 @@ module Issuable
end
def close_issue
close_service = Issues::CloseService.new(project: old_project, current_user: current_user)
close_service = Issues::CloseService.new(container: old_project, current_user: current_user)
close_service.execute(original_entity, notifications: false, system_note: true)
end

View File

@ -2,6 +2,11 @@
module Issues
class AfterCreateService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue)
todo_service.new_issue(issue, current_user)
delete_milestone_total_issue_counter_cache(issue.milestone)

View File

@ -2,6 +2,11 @@
module Issues
class CloseService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
# Closes the supplied issue if the current user is able to do so.
def execute(issue, commit: nil, notifications: true, system_note: true, skip_authorization: false)
return issue unless can_close?(issue, skip_authorization: skip_authorization)
@ -51,6 +56,11 @@ module Issues
private
# TODO: remove once MergeRequests::CloseService or IssuableBaseService method is changed.
def self.constructor_container_arg(value)
{ container: value }
end
def can_close?(issue, skip_authorization: false)
skip_authorization || can?(current_user, :update_issue, issue) || issue.is_a?(ExternalIssue)
end

View File

@ -2,6 +2,11 @@
module Issues
class DuplicateService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(duplicate_issue, canonical_issue)
return if canonical_issue == duplicate_issue
return unless can?(current_user, :update_issue, duplicate_issue)
@ -10,7 +15,7 @@ module Issues
create_issue_duplicate_note(duplicate_issue, canonical_issue)
create_issue_canonical_note(canonical_issue, duplicate_issue)
close_service.new(project: project, current_user: current_user).execute(duplicate_issue)
close_service.new(container: project, current_user: current_user).execute(duplicate_issue)
duplicate_issue.update(duplicated_to: canonical_issue)
relate_two_issues(duplicate_issue, canonical_issue)

View File

@ -2,6 +2,11 @@
module Issues
class ReferencedMergeRequestsService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(issue)
referenced = referenced_merge_requests(issue)

View File

@ -4,6 +4,11 @@
# those with a merge request open referencing the current issue.
module Issues
class RelatedBranchesService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue)
branch_names_with_mrs = branches_with_merge_request_for(issue)
branches = branches_with_iid_of(issue).reject { |b| branch_names_with_mrs.include?(b[:name]) }
@ -27,7 +32,7 @@ module Issues
def branches_with_merge_request_for(issue)
Issues::ReferencedMergeRequestsService
.new(project: project, current_user: current_user)
.new(container: project, current_user: current_user)
.referenced_merge_requests(issue)
.map(&:source_branch)
end

View File

@ -2,6 +2,11 @@
module Issues
class ReopenService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue, skip_authorization: false)
return issue unless can_reopen?(issue, skip_authorization: skip_authorization)
@ -22,6 +27,14 @@ module Issues
private
# overriding this because IssuableBaseService#constructor_container_arg returns { project: value }
# Issues::ReopenService constructor signature is different now, it takes container instead of project also
# IssuableBaseService#change_state dynamically picks one of the `Issues::ReopenService`, `Epics::ReopenService` or
# MergeRequests::ReopenService, so we need this method to return { }container: value } for Issues::ReopenService
def self.constructor_container_arg(value)
{ container: value }
end
def can_reopen?(issue, skip_authorization: false)
skip_authorization || can?(current_user, :reopen_issue, issue)
end

View File

@ -4,6 +4,11 @@ module Issues
class ReorderService < Issues::BaseService
include Gitlab::Utils::StrongMemoize
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false unless move_between_ids
@ -14,7 +19,7 @@ module Issues
private
def update(issue, attrs)
::Issues::UpdateService.new(project: project, current_user: current_user, params: attrs).execute(issue)
::Issues::UpdateService.new(container: project, current_user: current_user, params: attrs).execute(issue)
rescue ActiveRecord::RecordNotFound
false
end

View File

@ -5,8 +5,8 @@ module Issues
# NOTE: For Issues::UpdateService, we default the spam_params to nil, because spam_checking is not
# necessary in many cases, and we don't want to require every caller to explicitly pass it as nil
# to disable spam checking.
def initialize(project:, current_user: nil, params: {}, spam_params: nil)
super(project: project, current_user: current_user, params: params)
def initialize(container:, current_user: nil, params: {}, spam_params: nil)
super(project: container, current_user: current_user, params: params)
@spam_params = spam_params
end
@ -96,7 +96,7 @@ module Issues
canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
if canonical_issue
Issues::DuplicateService.new(project: project, current_user: current_user).execute(issue, canonical_issue)
Issues::DuplicateService.new(container: project, current_user: current_user).execute(issue, canonical_issue)
end
end
# rubocop: enable CodeReuse/ActiveRecord
@ -116,6 +116,15 @@ module Issues
attr_reader :spam_params
# TODO: remove this once MergeRequests::UpdateService#initialize is changed to take container as named argument.
#
# Issues::UpdateService is used together with MergeRequests::UpdateService in Mutations::Assignable#assign! method
# however MergeRequests::UpdateService#initialize still takes `project` as param and Issues::UpdateService is being
# changed to take `container` as param. So we are adding this workaround in the meantime.
def self.constructor_container_arg(value)
{ container: value }
end
def handle_quick_actions(issue)
# Do not handle quick actions unless the work item is the default Issue.
# The available quick actions for a work item depend on its type and widgets.

View File

@ -2,8 +2,8 @@
module Issues
class ZoomLinkService < Issues::BaseService
def initialize(project:, current_user:, params:)
super
def initialize(container:, current_user:, params:)
super(project: container, current_user: current_user, params: params)
@issue = params.fetch(:issue)
@added_meeting = ZoomMeeting.canonical_meeting(@issue)

View File

@ -14,7 +14,7 @@ module MergeRequests
def execute
assignable_issues.each do |issue|
Issues::UpdateService.new(project: issue.project, current_user: current_user, params: { assignee_ids: [current_user.id] }).execute(issue)
Issues::UpdateService.new(container: issue.project, current_user: current_user, params: { assignee_ids: [current_user.id] }).execute(issue)
end
{

View File

@ -55,7 +55,7 @@ module MergeRequests
merge_request.id
)
else
Issues::CloseService.new(project: project, current_user: current_user).execute(issue, commit: merge_request)
Issues::CloseService.new(container: project, current_user: current_user).execute(issue, commit: merge_request)
end
end
end

View File

@ -7,7 +7,7 @@ module Milestones
update_params = { milestone_id: nil, skip_milestone_email: true }
milestone.issues.each do |issue|
Issues::UpdateService.new(project: parent, current_user: current_user, params: update_params).execute(issue)
Issues::UpdateService.new(container: parent, current_user: current_user, params: update_params).execute(issue)
end
milestone.merge_requests.each do |merge_request|

View File

@ -16,7 +16,7 @@ module TasksToBeDone
def execute
if (issue = existing_task_issue)
update_service = Issues::UpdateService.new(project: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
update_service = Issues::UpdateService.new(container: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
update_service.execute(issue)
else
build_service = Issues::BuildService.new(container: project, current_user: current_user, params: params)

View File

@ -39,7 +39,7 @@ module WorkItems
end
::WorkItems::UpdateService.new(
project: @work_item.project,
container: @work_item.project,
current_user: @current_user,
params: { description: source_lines.join("\n"), lock_version: @lock_version }
).execute(@work_item)

View File

@ -34,7 +34,7 @@ module WorkItems
remove_additional_lines!(source_lines)
::WorkItems::UpdateService.new(
project: @work_item.project,
container: @work_item.project,
current_user: @current_user,
params: { description: source_lines.join("\n"), lock_version: @lock_version }
).execute(@work_item)

View File

@ -4,10 +4,10 @@ module WorkItems
class UpdateService < ::Issues::UpdateService
include WidgetableService
def initialize(project:, current_user: nil, params: {}, spam_params: nil, widget_params: {})
def initialize(container:, current_user: nil, params: {}, spam_params: nil, widget_params: {})
params[:widget_params] = true if widget_params.present?
super(project: project, current_user: current_user, params: params, spam_params: nil)
super(container: container, current_user: current_user, params: params, spam_params: spam_params)
@widget_params = widget_params
end

View File

@ -17,6 +17,9 @@
= f.gitlab_ui_checkbox_component :external_authorization_service_enabled,
s_('ExternalAuthorization|Enable classification control using an external service'),
help_text: external_authorization_description
= f.gitlab_ui_checkbox_component :allow_deploy_tokens_and_keys_with_external_authn,
s_('ExternalAuthorization|Allow deploy tokens and deploy keys to be used with external authorization'),
help_text: external_authorization_allow_token_help_text
.form-group
= f.label :external_authorization_service_url, s_('ExternalAuthorization|Service URL'), class: 'label-bold'
= f.text_field :external_authorization_service_url, class: 'form-control gl-form-input'

View File

@ -30,7 +30,7 @@ module IncidentManagement
def close_incident(incident)
::Issues::CloseService
.new(project: incident.project, current_user: user)
.new(container: incident.project, current_user: user)
.execute(incident, system_note: false)
end

View File

@ -42,7 +42,7 @@ module Issues
end
commit = Commit.build_from_sidekiq_hash(project, params["commit_hash"])
service = Issues::CloseService.new(project: project, current_user: author)
service = Issues::CloseService.new(container: project, current_user: author)
service.execute(issue, commit: commit)
end

View File

@ -45,7 +45,7 @@ module MergeRequests
end
Issues::CloseService
.new(project: project, current_user: user)
.new(container: project, current_user: user)
.execute(issue, commit: merge_request)
end
end

View File

@ -26,7 +26,7 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
issuable.create_cross_references!(user)
Issues::AfterCreateService
.new(project: issuable.project, current_user: user)
.new(container: issuable.project, current_user: user)
.execute(issuable)
end
end

View File

@ -0,0 +1,8 @@
---
name: data_transfer_monitoring
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110211
rollout_issue_url:
milestone: '15.9'
type: development
group: group::source code
default_enabled: false

View File

@ -903,6 +903,8 @@ Settings['repositories'] ||= Settingslogic.new({})
Settings.repositories['storages'] ||= {}
Settings.repositories.storages.each do |key, storage|
next if Settings.repositories.storages[key].is_a?(Gitlab::GitalyClient::StorageSettings)
Settings.repositories.storages[key] = Gitlab::GitalyClient::StorageSettings.new(storage)
end

View File

@ -1,4 +0,0 @@
# frozen_string_literal: true
FastGettext.default_available_locales = Gitlab::I18n.available_locales
I18n.available_locales = Gitlab::I18n.available_locales

View File

@ -1,31 +1,3 @@
# frozen_string_literal: true
translation_repositories = [
FastGettext::TranslationRepository.build(
'gitlab',
path: File.join(Rails.root, 'locale'),
type: :po,
ignore_fuzzy: true
)
]
Gitlab.jh do
translation_repositories.unshift(
FastGettext::TranslationRepository.build(
'gitlab',
path: File.join(Rails.root, 'jh', 'locale'),
type: :po,
ignore_fuzzy: true
)
)
end
FastGettext.add_text_domain(
'gitlab',
type: :chain,
chain: translation_repositories,
ignore_fuzzy: true
)
FastGettext.default_text_domain = 'gitlab'
FastGettext.default_locale = :en
Gitlab::I18n.setup(domain: 'gitlab', default_locale: :en)

View File

@ -178,8 +178,12 @@ class ObjectStoreSettings
# 1. The common settings are defined
# 2. The legacy settings are not defined
def use_consolidated_settings?
return false unless settings.dig('object_store', 'enabled')
return false unless settings.dig('object_store', 'connection').present?
# to_h is needed because we define `default` as a Gitaly storage name
# in stub_storage_settings. This causes Settingslogic to redefine Hash#default,
# which causes Hash#dig to fail when the key doesn't exist: https://gitlab.com/gitlab-org/gitlab/-/issues/286873
settings_h = settings.to_h
return false unless settings_h.dig('object_store', 'enabled')
return false unless settings_h.dig('object_store', 'connection').present?
WORKHORSE_ACCELERATED_TYPES.each do |store|
# to_h is needed because we define `default` as a Gitaly storage name

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddAllowDeployTokensAndKeysWithExternalAuthnToApplicationSettings < Gitlab::Database::Migration[2.1]
def change
add_column(:application_settings, :allow_deploy_tokens_and_keys_with_external_authn, :boolean,
default: false, null: false)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class RemoveTextLimitFromCiJobArtifactsOriginalFilename < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
# In preparation for 20230214142813_remove_ci_job_artifacts_original_filename.rb
# We first remove the text limit before removing the column.
# This is to properly reverse the 2-step migration to add a text column with limit
# https://docs.gitlab.com/ee/development/database/strings_and_the_text_data_type.html#add-a-text-column-to-an-existing-table
remove_text_limit :ci_job_artifacts, :original_filename
end
def down
add_text_limit :ci_job_artifacts, :original_filename, 512
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class RemoveCiJobArtifactsOriginalFilename < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def up
# This column has never been used and has always been under ignore_column since it was added.
# We're doing the removal of the ignore_column in the same MR with this migration and this
# is why we are not doing this in post migrate.
remove_column :ci_job_artifacts, :original_filename, :text # rubocop:disable Migration/RemoveColumn
end
def down
add_column :ci_job_artifacts, :original_filename, :text
end
end

View File

@ -0,0 +1 @@
9e7245187ad1618304f2cdc901a6d8f63e63d007578da92f7ba049def9312923

View File

@ -0,0 +1 @@
29006be848d8a5ba33c0e757ac4743cc19dc0274893e2e23c73615218975feef

View File

@ -0,0 +1 @@
c7f6778eb181c6c4e97b7d7698bb7df5a4589710426d0a6574d5230f9751ebed

View File

@ -11722,6 +11722,7 @@ CREATE TABLE application_settings (
deactivation_email_additional_text text,
jira_connect_public_key_storage_enabled boolean DEFAULT false NOT NULL,
git_rate_limit_users_alertlist integer[] DEFAULT '{}'::integer[] NOT NULL,
allow_deploy_tokens_and_keys_with_external_authn boolean DEFAULT false NOT NULL,
security_policy_global_group_approvers_enabled boolean DEFAULT true NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
@ -13141,11 +13142,9 @@ CREATE TABLE ci_job_artifacts (
id bigint NOT NULL,
job_id bigint NOT NULL,
locked smallint DEFAULT 2,
original_filename text,
partition_id bigint DEFAULT 100 NOT NULL,
accessibility smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL)),
CONSTRAINT check_85573000db CHECK ((char_length(original_filename) <= 512))
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL))
);
CREATE SEQUENCE ci_job_artifacts_id_seq

View File

@ -220,6 +220,8 @@ An [epic exists](https://gitlab.com/groups/gitlab-org/-/epics/4624) to fix this
Keep in mind that mentioned URLs don't work when [Admin Mode](../../user/admin_area/settings/sign_in_restrictions.md#admin-mode) is enabled.
When using Unified URL, visiting the secondary site directly means you must route your requests to the secondary site. Exactly how this might be done depends on your networking configuration. If using DNS to route requests to the appropriate site, then you can, for example, edit your local machine's `/etc/hosts` file to route your requests to the desired secondary site. If the Geo sites are all behind a load balancer, then depending on the load balancer, you might be able to configure all requests from your IP to go to a particular secondary site.
## Setup instructions
For setup instructions, see [Setting up Geo](setup/index.md).

View File

@ -7843,6 +7843,29 @@ The edge type for [`Discussion`](#discussion).
| <a id="discussionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="discussionedgenode"></a>`node` | [`Discussion`](#discussion) | The item at the end of the edge. |
#### `EgressNodeConnection`
The connection type for [`EgressNode`](#egressnode).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="egressnodeconnectionedges"></a>`edges` | [`[EgressNodeEdge]`](#egressnodeedge) | A list of edges. |
| <a id="egressnodeconnectionnodes"></a>`nodes` | [`[EgressNode]`](#egressnode) | A list of nodes. |
| <a id="egressnodeconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `EgressNodeEdge`
The edge type for [`EgressNode`](#egressnode).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="egressnodeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="egressnodeedgenode"></a>`node` | [`EgressNode`](#egressnode) | The item at the end of the edge. |
#### `EmailConnection`
The connection type for [`Email`](#email).
@ -12877,6 +12900,19 @@ Returns [`[DoraMetric!]`](#dorametric).
| <a id="dorametricdate"></a>`date` | [`String`](#string) | Date of the data point. |
| <a id="dorametricvalue"></a>`value` | [`Float`](#float) | Value of the data point. |
### `EgressNode`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="egressnodeartifactsegress"></a>`artifactsEgress` | [`BigInt!`](#bigint) | Artifacts egress for that project in that period of time. |
| <a id="egressnodedate"></a>`date` | [`String!`](#string) | First day of the node range. There is one node per month. |
| <a id="egressnodepackagesegress"></a>`packagesEgress` | [`BigInt!`](#bigint) | Packages egress for that project in that period of time. |
| <a id="egressnoderegistryegress"></a>`registryEgress` | [`BigInt!`](#bigint) | Registery egress for that project in that period of time. |
| <a id="egressnoderepositoryegress"></a>`repositoryEgress` | [`BigInt!`](#bigint) | Repository egress for that project in that period of time. |
| <a id="egressnodetotalegress"></a>`totalEgress` | [`BigInt!`](#bigint) | Total egress for that project in that period of time. |
### `Email`
#### Fields
@ -14079,6 +14115,19 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupcontributionsfrom"></a>`from` | [`ISO8601Date!`](#iso8601date) | Start date of the reporting time range. |
| <a id="groupcontributionsto"></a>`to` | [`ISO8601Date!`](#iso8601date) | End date of the reporting time range. The end date must be within 31 days after the start date. |
##### `Group.dataTransfer`
Data transfer data point for a specific period. This is mocked data under a development feature flag.
Returns [`GroupDataTransfer`](#groupdatatransfer).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupdatatransferfrom"></a>`from` | [`Date`](#date) | Retain egress data for 1 year. Current month will increase dynamically as egress occurs. |
| <a id="groupdatatransferto"></a>`to` | [`Date`](#date) | End date for the data. |
##### `Group.descendantGroups`
List of descendant groups of this group.
@ -14688,6 +14737,14 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="groupworkitemtypestaskable"></a>`taskable` | [`Boolean`](#boolean) | If `true`, only taskable work item types will be returned. Argument is experimental and can be removed in the future without notice. |
### `GroupDataTransfer`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupdatatransferegressnodes"></a>`egressNodes` | [`EgressNodeConnection`](#egressnodeconnection) | Data nodes. (see [Connections](#connections)) |
### `GroupMember`
Represents a Group Membership.
@ -17979,6 +18036,19 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectdastsitevalidationsnormalizedtargeturls"></a>`normalizedTargetUrls` | [`[String!]`](#string) | Normalized URL of the target to be scanned. |
| <a id="projectdastsitevalidationsstatus"></a>`status` | [`DastSiteValidationStatusEnum`](#dastsitevalidationstatusenum) | Status of the site validation. |
##### `Project.dataTransfer`
Data transfer data point for a specific period. This is mocked data under a development feature flag.
Returns [`ProjectDataTransfer`](#projectdatatransfer).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectdatatransferfrom"></a>`from` | [`Date`](#date) | Retain egress data for 1 year. Current month will increase dynamically as egress occurs. |
| <a id="projectdatatransferto"></a>`to` | [`Date`](#date) | End date for the data. |
##### `Project.deployment`
Details of the deployment of the project.
@ -18561,6 +18631,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectpipelineschedulesids"></a>`ids` | [`[ID!]`](#id) | Filter pipeline schedules by IDs. |
| <a id="projectpipelineschedulesstatus"></a>`status` | [`PipelineScheduleStatus`](#pipelineschedulestatus) | Filter pipeline schedules by active status. |
##### `Project.pipelines`
@ -18986,6 +19057,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectcicdsettingoptinjwt"></a>`optInJwt` | [`Boolean`](#boolean) | When disabled, the JSON Web Token is always available in all jobs in the pipeline. |
| <a id="projectcicdsettingproject"></a>`project` | [`Project`](#project) | Project the CI/CD settings belong to. |
### `ProjectDataTransfer`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectdatatransferegressnodes"></a>`egressNodes` | [`EgressNodeConnection`](#egressnodeconnection) | Data nodes. (see [Connections](#connections)) |
| <a id="projectdatatransfertotalegress"></a>`totalEgress` | [`BigInt`](#bigint) | Total egress for that project in that period of time. |
### `ProjectMember`
Represents a Project Membership.

View File

@ -373,6 +373,14 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
Sites that have configured `max_concurrency` will not be affected by this change.
[Read more about the Sidekiq concurrency setting](../administration/sidekiq/extra_sidekiq_processes.md#concurrency).
- GitLab Runner 15.7.0 introduced a breaking change that impacts CI/CD jobs: [Correctly handle expansion of job file variables](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3613).
Previously, job-defined variables that referred to
[file type variables](../ci/variables/index.md#use-file-type-cicd-variables)
were expanded to the value of the file variable (its content). This behavior did not
respect the typical rules of shell variable expansion. There was also the potential
that secrets or sensitive information could leak if the file variable and its
contents printed. For example, if they were printed in an echo output. For more information,
see [Understanding the file type variable expansion change in GitLab 15.7](https://about.gitlab.com/blog/2023/02/13/impact-of-the-file-type-variable-change-15-7/).
- Geo: [Container registry push events are rejected](https://gitlab.com/gitlab-org/gitlab/-/issues/386389) by the `/api/v4/container_registry_event/events` endpoint resulting in Geo secondary sites not being aware of updates to container registry images and subsequently not replicating the updates. Secondary sites may contain out of date container images after a failover as a consequence. This impacts versions 15.6.0 - 15.6.6 and 15.7.0 - 15.7.2. If you're using Geo with container repositories, you are advised to upgrade to GitLab 15.6.7, 15.7.3, or 15.8.0 which contain a fix for this issue and avoid potential data loss after a failover.
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
- Geo: We discovered an issue where [replication and verification of projects and wikis was not keeping up](https://gitlab.com/gitlab-org/gitlab/-/issues/387980) on small number of Geo installations. Your installation may be affected if you see some projects and/or wikis persistently in the "Queued" state for verification. This can lead to data loss after a failover.

View File

@ -65,6 +65,8 @@ For user contributions to be mapped, each user must complete the following befor
## Import your Bitbucket repositories
> Ability to re-import projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23905) in GitLab 15.9.
1. Sign in to GitLab.
1. On the top bar, select **New** (**{plus}**).
1. Select **New project/repository**.
@ -78,7 +80,9 @@ For user contributions to be mapped, each user must complete the following befor
You can filter projects by name and select the namespace
each project is imported for.
![Import projects](img/bitbucket_import_select_project_v12_3.png)
1. To import a project:
- For the first time: Select **Import**.
- Again: Select **Re-import**. Specify a new name and select **Re-import** again. Re-importing creates a new copy of the source project.
## Troubleshooting

View File

@ -24,6 +24,8 @@ created as private in GitLab as well.
## Import your Bitbucket repositories
> Ability to re-import projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23905) in GitLab 15.9.
Prerequisites:
- An administrator must enable **Bitbucket Server** in **Admin > Settings > General > Visibility and access controls > Import sources**.
@ -40,6 +42,9 @@ To import your Bitbucket repositories:
1. Log in to Bitbucket and grant GitLab access to your Bitbucket account.
1. Select the projects to import, or import all projects. You can filter projects by name and select
the namespace for which to import each project.
1. To import a project:
- For the first time: Select **Import**.
- Again: Select **Re-import**. Specify a new name and select **Re-import** again. Re-importing creates a new copy of the source project.
### Items that are not imported

View File

@ -7,6 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Import your project from FogBugz to GitLab **(FREE)**
> Ability to re-import projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23905) in GitLab 15.9.
Using the importer, you can import your FogBugz project to GitLab.com
or to your self-managed GitLab instance.
@ -33,4 +35,6 @@ To import your project from FogBugz:
![Import Project](img/fogbugz_import_select_project.png)
1. After the import finishes, select the link to go to the project
dashboard. Follow the directions to push your existing repository.
![Finished](img/fogbugz_import_finished.png)
1. To import a project:
- For the first time: Select **Import**.
- Again: Select **Re-import**. Specify a new name and select **Re-import** again. Re-importing creates a new copy of the source project.

View File

@ -70,8 +70,8 @@ From there, you can view the import statuses of your Gitea repositories:
- Those that are being imported show a _started_ status.
- Those already successfully imported are green with a _done_ status.
- Those that aren't yet imported have an **Import** button on the
right side of the table.
- Those that aren't yet imported have **Import** on the right side of the table.
- Those that are already imported have **Re-import** on the right side of the table.
You also can:

View File

@ -146,7 +146,8 @@ You can choose to import these items, but this could significantly increase impo
### Select which repositories to import
> Ability to cancel pending or active imports [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247325) in GitLab 15.7.
> - Ability to cancel pending or active imports [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247325) in GitLab 15.7.
> - Ability to re-import projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23905) in GitLab 15.9.
After you have authorized access to your GitHub repositories, you are redirected to the GitHub importer page and
your GitHub repositories are listed.
@ -168,6 +169,8 @@ If the import has already started, the imported files are kept.
To open an repository in GitLab URL after it has been imported, select its GitLab path.
Completed imports can be re-imported by selecting **Re-import** and specifying new name. This creates a new copy of the source project.
![GitHub importer page](img/import_projects_from_github_importer_v12_3.png)
## Mirror a repository and share pipeline status **(PREMIUM)**

View File

@ -7,7 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Import multiple repositories by uploading a manifest file **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/28811) in GitLab 11.2.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/28811) in GitLab 11.2.
> - Ability to re-import projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23905) in GitLab 15.9.
GitLab allows you to import all the required Git repositories
based on a manifest file like the one used by the
@ -59,6 +60,6 @@ To start the import:
1. Select a group you want to import to (you need to create a group first if you don't have one).
1. Select **List available repositories**. At this point, you are redirected
to the import status page with projects list based on the manifest file.
1. Check the list and select **Import all repositories** to start the import.
![Manifest status](img/manifest_status_v13_3.png)
1. To import:
- All projects for the first time: Select **Import all repositories**.
- Individual projects again: Select **Re-import**. Specify a new name and select **Re-import** again. Re-importing creates a new copy of the source project.

View File

@ -24,6 +24,12 @@ module API
def delete_draft_note(draft_note)
::DraftNotes::DestroyService.new(user_project, current_user).execute(draft_note)
end
def publish_draft_note(params:)
::DraftNotes::PublishService
.new(merge_request(params: params), current_user)
.execute(get_draft_note(params: params))
end
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
@ -90,6 +96,31 @@ module API
not_found!("Draft Note")
end
end
desc "Publish a pending draft note" do
success code: 204
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 404, message: 'Not found' }
]
end
params do
requires :id, type: String, desc: "The ID of a project"
requires :merge_request_iid, type: Integer, desc: "The ID of a merge request"
requires :draft_note_id, type: Integer, desc: "The ID of a draft note"
end
put(
":id/merge_requests/:merge_request_iid/draft_notes/:draft_note_id/publish",
feature_category: :code_review_workflow) do
result = publish_draft_note(params: params)
if result[:status] == :success
status 204
body false
else
status 500
end
end
end
end
end

View File

@ -319,7 +319,7 @@ module API
update_params = convert_parameters_from_legacy_format(update_params)
spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::UpdateService.new(project: user_project,
issue = ::Issues::UpdateService.new(container: user_project,
current_user: current_user,
params: update_params,
spam_params: spam_params).execute(issue)
@ -350,7 +350,7 @@ module API
authorize! :update_issue, issue
if ::Issues::ReorderService.new(project: user_project, current_user: current_user, params: params).execute(issue)
if ::Issues::ReorderService.new(container: user_project, current_user: current_user, params: params).execute(issue)
present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_api_error!({ error: 'Unprocessable Entity' }, 422)
@ -438,9 +438,10 @@ module API
get ':id/issues/:issue_iid/related_merge_requests' do
issue = find_project_issue(params[:issue_iid])
merge_requests = ::Issues::ReferencedMergeRequestsService.new(project: user_project, current_user: current_user)
.execute(issue)
.first
merge_requests = ::Issues::ReferencedMergeRequestsService
.new(container: user_project, current_user: current_user)
.execute(issue)
.first
present paginate(::Kaminari.paginate_array(merge_requests)),
with: Entities::MergeRequest,

View File

@ -35,7 +35,9 @@ module API
custom_params = declared_params(include_missing: false)
custom_params.merge!(attrs)
issuable = update_service.new(project: user_project, current_user: current_user, params: custom_params).execute(load_issuable)
issuable = update_service.new(**update_service.constructor_container_arg(user_project),
current_user: current_user, params: custom_params).execute(load_issuable)
if issuable.valid?
present issuable, with: Entities::IssuableTimeStats
else

View File

@ -165,7 +165,9 @@ module Gitlab
end
def with_deploy_token(raw, &block)
raise ::Gitlab::Auth::UnauthorizedError if Gitlab::ExternalAuthorization.enabled?
unless Gitlab::ExternalAuthorization.allow_deploy_tokens_and_deploy_keys?
raise ::Gitlab::Auth::UnauthorizedError
end
token = ::DeployToken.active.find_by_token(raw.password)
return unless token

View File

@ -148,7 +148,7 @@ module Gitlab
# deploy tokens are accepted with deploy token headers and basic auth headers
def deploy_token_from_request
return unless route_authentication_setting[:deploy_token_allowed]
return if Gitlab::ExternalAuthorization.enabled?
return unless Gitlab::ExternalAuthorization.allow_deploy_tokens_and_deploy_keys?
token = current_request.env[DEPLOY_TOKEN_HEADER].presence || parsed_oauth_token

View File

@ -28,7 +28,7 @@ module Gitlab
file.validate_content! if file.valid?
file.load_and_validate_expanded_hash! if file.valid?
if ::Feature.enabled?(:ci_includes_count_duplicates, context.project)
if context.expandset.is_a?(Array) # To be removed when FF 'ci_includes_count_duplicates' is removed
context.expandset << file
else
context.expandset.add(file)

View File

@ -37,6 +37,12 @@ module Gitlab
client_cert.present? && client_key.present?
end
def allow_deploy_tokens_and_deploy_keys?
return true unless enabled?
service_url.blank? && application_settings.allow_deploy_tokens_and_keys_with_external_authn?
end
private
def application_settings

View File

@ -367,7 +367,7 @@ module Gitlab
end
def deploy_key?
actor.is_a?(DeployKey) && !Gitlab::ExternalAuthorization.enabled?
actor.is_a?(DeployKey) && Gitlab::ExternalAuthorization.allow_deploy_tokens_and_deploy_keys?
end
def deploy_token
@ -375,7 +375,7 @@ module Gitlab
end
def deploy_token?
actor.is_a?(DeployToken) && !Gitlab::ExternalAuthorization.enabled?
actor.is_a?(DeployToken) && Gitlab::ExternalAuthorization.allow_deploy_tokens_and_deploy_keys?
end
def ci?

View File

@ -116,5 +116,43 @@ module Gitlab
def with_default_locale(&block)
with_locale(::I18n.default_locale, &block)
end
def setup(domain:, default_locale:)
setup_repositories(domain)
setup_default_locale(default_locale)
end
private
def setup_repositories(domain)
translation_repositories = [
(po_repository(domain, 'jh/locale') if Gitlab.jh?),
po_repository(domain, 'locale')
].compact
FastGettext.add_text_domain(
domain,
type: :chain,
chain: translation_repositories,
ignore_fuzzy: true
)
FastGettext.default_text_domain = domain
end
def po_repository(domain, path)
FastGettext::TranslationRepository.build(
domain,
path: Rails.root.join(path),
type: :po,
ignore_fuzzy: true
)
end
def setup_default_locale(locale)
FastGettext.default_locale = locale
FastGettext.default_available_locales = available_locales
::I18n.available_locales = available_locales
end
end
end

View File

@ -248,7 +248,7 @@ module Gitlab
if severity
if quick_action_target.persisted?
::Issues::UpdateService.new(project: quick_action_target.project, current_user: current_user, params: { severity: severity }).execute(quick_action_target)
::Issues::UpdateService.new(container: quick_action_target.project, current_user: current_user, params: { severity: severity }).execute(quick_action_target)
else
quick_action_target.build_issuable_severity(severity: severity)
end

View File

@ -320,7 +320,7 @@ module Gitlab
private
def zoom_link_service
::Issues::ZoomLinkService.new(project: quick_action_target.project, current_user: current_user, params: { issue: quick_action_target })
::Issues::ZoomLinkService.new(container: quick_action_target.project, current_user: current_user, params: { issue: quick_action_target })
end
def zoom_link_params

View File

@ -204,6 +204,14 @@ module Gitlab
extra.merge(command_name: command_name, instance_name: instance_name))
end
def default_store
use_primary_store_as_default? ? primary_store : secondary_store
end
def fallback_store
use_primary_store_as_default? ? secondary_store : primary_store
end
def ping(message = nil)
if use_primary_and_secondary_stores?
# Both stores have to response success for the ping to be considered success.
@ -226,10 +234,6 @@ module Gitlab
false
end
def default_store
use_primary_store_as_default? ? primary_store : secondary_store
end
def log_method_missing(command_name, *_args)
return if SKIP_LOG_METHOD_MISSING_FOR_COMMANDS.include?(command_name)
@ -257,7 +261,7 @@ module Gitlab
def read_one_with_fallback(command_name, *args, **kwargs, &block)
begin
value = send_command(primary_store, command_name, *args, **kwargs, &block)
value = send_command(default_store, command_name, *args, **kwargs, &block)
rescue StandardError => e
log_error(e, command_name,
multi_store_error_message: FAILED_TO_READ_ERROR_MESSAGE)
@ -276,7 +280,7 @@ module Gitlab
end
def fallback_read(command_name, *args, **kwargs, &block)
value = send_command(secondary_store, command_name, *args, **kwargs, &block)
value = send_command(fallback_store, command_name, *args, **kwargs, &block)
if value
log_error(ReadFromPrimaryError.new, command_name)

View File

@ -29,7 +29,7 @@ module Gitlab
private
def close_issue(issue:)
::Issues::CloseService.new(project: project, current_user: current_user).execute(issue)
::Issues::CloseService.new(container: project, current_user: current_user).execute(issue)
end
def presenter(issue)

View File

@ -17010,6 +17010,9 @@ msgstr ""
msgid "ExternalAuthorization|Access to projects is validated on an external service using their classification label."
msgstr ""
msgid "ExternalAuthorization|Allow deploy tokens and deploy keys to be used with external authorization"
msgstr ""
msgid "ExternalAuthorization|Certificate used to authenticate with the external authorization service. If blank, the server certificate is validated when accessing over HTTPS."
msgstr ""
@ -17028,6 +17031,9 @@ msgstr ""
msgid "ExternalAuthorization|Default classification label"
msgstr ""
msgid "ExternalAuthorization|Does not apply if service URL is specified."
msgstr ""
msgid "ExternalAuthorization|Enable classification control using an external service"
msgstr ""
@ -21545,6 +21551,9 @@ msgstr ""
msgid "ImportProjects|Importing the project failed: %{reason}"
msgstr ""
msgid "ImportProjects|Re-import creates a new project. It does not sync with the existing project."
msgstr ""
msgid "ImportProjects|Requesting namespaces failed"
msgstr ""

View File

@ -15,7 +15,7 @@ module QA
element :project_path_field
element :import_button
element :project_path_content
element :go_to_project_button
element :go_to_project_link
element :import_status_indicator
end
@ -60,9 +60,9 @@ module QA
#
# @param [String] gh_project_name
# @return [Boolean]
def has_go_to_project_button?(gh_project_name)
def has_go_to_project_link?(gh_project_name)
within_element(:project_import_row, source_project: gh_project_name) do
has_element?(:go_to_project_button)
has_element?(:go_to_project_link)
end
end

View File

@ -52,9 +52,9 @@ module QA
aggregate_failures do
expect(import_page).to have_imported_project(github_repo, wait: 240)
# validate button is present instead of navigating to avoid dealing with multiple tabs
# validate link is present instead of navigating to avoid dealing with multiple tabs
# which makes the test more complicated
expect(import_page).to have_go_to_project_button(github_repo)
expect(import_page).to have_go_to_project_link(github_repo)
end
end

View File

@ -0,0 +1,45 @@
#!/usr/bin/env node
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3')
const { fromIni } = require('@aws-sdk/credential-provider-ini')
const path = require('path')
const fs = require('fs')
const crypto = require('crypto')
function getMD5HashFromFile(data) {
const hash = crypto.createHash('md5').update(data).digest('base64')
return hash
}
(async function () {
const s3Client = new S3Client({
region: 'us-east-2',
credentials: fromIni({ profile: 'gl-logs-for-panther' }),
})
try {
const file = 'gl-dependency-scanning-report.json'
const data = fs.readFileSync(file)
const [filename, fileext] = path.basename(file).split('.')
const uniqueId = process.env['CI_PIPELINE_ID'] && process.env['CI_JOB_ID'] ?
process.env['CI_PIPELINE_ID'] + '-' + process.env['CI_JOB_ID'] :
Date.now()
const key = path.join('package_hunter_logs', filename + '-' + uniqueId + '.' + fileext)
const responseData = await s3Client.send(
new PutObjectCommand({
Bucket: 'package-hunter-logs',
Key: key,
Body: data,
ContentMD5: getMD5HashFromFile(data),
}),
)
console.log('Successfully uploaded %s to %s', file, key)
} catch (err) {
if (err.name === 'CredentialsProviderError' || err.name === 'AuthorizationHeaderMalformed')
console.log('Could not upload the report. Are AWS credentials configured in ~/.aws/credentials?')
else
console.log('Unexpected error during upload: ', err.message)
process.exit(1)
}
})()

View File

@ -190,4 +190,12 @@ RSpec.describe Settings, feature_category: :authentication_and_authorization do
expect(described_class.microsoft_graph_mailer.graph_endpoint).to eq('https://graph.microsoft.com')
end
end
describe '.repositories' do
it 'sets up storage settings' do
described_class.repositories.storages.each do |_, storage|
expect(storage).to be_a Gitlab::GitalyClient::StorageSettings
end
end
end
end

View File

@ -26,7 +26,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
visit edit_project_issue_path(project, issue)
end
it "previews content" do
it "previews content", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391757' do
form = first(".gfm-form")
page.within(form) do

Some files were not shown because too many files have changed in this diff Show More